Ir al contenido
Documentación

Primeros pasos

Payniko acepta pagos con tarjeta mediante una única API REST firmada. Puede redirigir al pagador a la página de pago de Payniko (Hosted API) o recopilar los datos de la tarjeta en su propio formulario de pago (Direct API).

URL base y formatos

URL base
https://payniko.com/api. La ruta no incluye ninguna versión.
Formato
JSON con codificación UTF-8. Los errores utilizan application/problem+json.
Importes
Cadenas con dos decimales, por ejemplo "100.00". No envíe nunca los importes como números JSON.
Moneda
USD, EUR, AED o TRY. Si se omite currency, se aplica la moneda principal del sistema.
Fechas y horas
ISO 8601 en UTC con milisegundos, por ejemplo 2026-11-01T12:00:00.000Z.
Identificadores
Los identificadores de pago, reembolso, retiro y disputa usan LUVEI-ABCD-123456. Guarde el identificador completo.
Idioma
Los mensajes se devuelven en el idioma indicado en Accept-Language (en, tr, de, ar, es, el); el valor predeterminado es en. Los códigos y los nombres de campo nunca se traducen.

Obtener las claves de API

Las cuentas nuevas reciben automáticamente claves Sandbox y Live. Si ya tiene una cuenta, inicie sesión y abra Claves de acceso. Si recibió un archivo de cuenta, acceda con su usuario y contraseña; no se registre de nuevo.

1. Cree su cuenta

  1. Cree una cuenta. Introduzca su correo habitual y pulse Enviar código. Escriba en el registro el código de seis dígitos recibido y pulse Verificar. Si no llega, revise la dirección y spam; solicite otro código al terminar la cuenta atrás.

  2. Tras verificar el correo, elija el prefijo de país e introduzca el número vinculado a su propia cuenta de Telegram. Pulse Verificar. La verificación se realiza en Telegram, no por SMS.

  3. Abra el enlace de Telegram mostrado o, desde un ordenador, escanee el QR con su teléfono. Pulse Iniciar / Start en el bot y comparta su propio número con el botón del bot. Escribirlo como mensaje o enviar el contacto de otra persona no lo verifica. Vuelva al registro y espere la confirmación.

  4. Pulse Continuar con los datos de la cuenta. Cree usuario y contraseña, repita la contraseña, complete las confirmaciones y pulse Crear la cuenta. El usuario admite 4–32 caracteres: minúsculas, números, punto, guion bajo y guion. La contraseña requiere al menos 12 caracteres.

  5. Guarde en un lugar seguro el código de recuperación que aparece al crear la cuenta. Solo se muestra en esa pantalla y no es el secreto de API. Confirme que lo guardó y pulse He guardado el código, ir al panel.

2. Obtenga las claves Sandbox y el código de comercio

  1. Abra Claves de acceso y busque la tarjeta Sandbox. Las cuentas nuevas ya tienen claves. Si una cuenta anterior no tiene una clave activa, añada una etiqueta opcional y pulse Generar clave. No necesita registrar una IP.

  2. Obtenga los tres datos de la tabla. El icono de ojo muestra el secreto y el icono de copiar lo copia. El titular puede volver a consultar un secreto activo; no es necesario renovar la clave para verlo.

  3. Comparta los datos de forma segura con su desarrollador o guárdelos en la configuración privada del servidor. La clave y el secreto deben ser de la misma tarjeta Sandbox. No publique el secreto en JavaScript del navegador, archivos públicos ni URL.

DatoDónde encontrarloUso
Código de comercioCódigo de seis dígitos de la tarjetaCabecera X-Merchant-Code; el mismo código de cuenta sirve en Sandbox y Live.
Clave de APIFila de clave de la tarjeta SandboxCabecera X-Api-Key; determina el entorno.
Secreto de APIIconos de ojo/copiar de esa tarjetaFirma solicitudes y verifica firmas de webhook; no se envía como campo de solicitud.

Crear un pago de prueba

Pruebe su integración en Sandbox sin mover dinero real. Una prueba correcta no es un requisito para las claves Live; se recomienda para comprobar firmas, redirecciones y webhooks.

  1. Use un ejemplo de servidor de Funciones auxiliares de solicitud con su clave Sandbox, secreto Sandbox y código de comercio. No hay un dominio de API Sandbox distinto: use la URL base documentada; la clave determina el entorno.

  2. Revise los límites de importe y monedas Sandbox en Experiencia de pago. Elija un importe permitido que no termine en .77: esa terminación activa el escenario de prueba pendiente.

  3. Rellene el ejemplo POST /api/payments/hosted: merchant_order_id único, importe, moneda, customer.email / customer.phone y las tres URL de notificación/retorno. Use datos de contacto accesibles y teléfono con prefijo internacional. webhook_url debe ser HTTPS público, incluso en Sandbox; no se admite localhost ni una red privada. success_url / fail_url son las URL de retorno del navegador.

  4. Envíe la solicitud firmada POST /api/payments/hosted desde su servidor. Para una prueba nueva use nueva referencia y nuevo Idempotency-Key; al repetir la misma solicitud tras un error de conexión, conserve el mismo valor. HTTP 201 indica que se creó el pago, no que se pagó.

  5. Abra en el navegador la next_action.url completa de la respuesta. En el formulario Sandbox use la tarjeta de prueba 4000000000000002, una caducidad futura y CVV de tres dígitos; por ejemplo 12/30 y 123. Complete el resultado aprobado del 3D Secure de prueba. No use una tarjeta real. Más escenarios: Tarjetas de prueba.

  6. Después del retorno, consulte GET /api/payments/{payment_id} o procese un webhook cuya firma haya verificado. El estado de API debe ser succeeded. requires_action / processing son estados intermedios; failed no cuenta como éxito. Abrir la página de éxito no demuestra por sí solo el pago.

  7. Compruebe el resultado en Registro de solicitudes. Un registro correcto completa este escenario de integración. Live utiliza su propio par de claves.

Obtener las claves Live

Sandbox y Live utilizan pares de claves separados. Las claves Live no requieren registrar una IP ni un pago Sandbox correcto. Cambiar el entorno en el panel no cambia la configuración del servidor.

  1. Abra Acceso Live y busque la tarjeta Producción. Las cuentas nuevas ya tienen claves Live; si una cuenta anterior no tiene ninguna, pulse Generar clave.

  2. Copie la clave API y el secreto de la misma tarjeta Live. El código de comercio es el mismo. Guarde el secreto únicamente en la configuración privada del servidor.

  3. Configure ese par de claves Live en el servidor de producción. La URL de API es la misma y Sandbox sigue válido para pruebas. Ver una tarjeta en el panel no modifica la configuración de su servidor.

  4. Revise límites y monedas Live en Experiencia de pago. Las claves están listas. Las solicitudes con claves Live entran en el flujo de cobro real; siga probando tarjetas de prueba con las claves Sandbox.

Si no puede continuar

SituaciónQué hacer
Correo o Telegram sin verificarIntroduzca el código de correo en el registro. Use el bot enlazado allí y comparta el mismo número con su botón. Renueve los enlaces caducados desde el registro.
No se puede generar una claveCompruebe que su cuenta esté activa y tenga habilitados los cobros API. No se exige registrar una IP ni completar un pago Sandbox.
No aparece la clave LiveAbra la tarjeta Live. Las cuentas nuevas ya tienen claves; genere una si una cuenta anterior no tiene ninguna activa. Si persiste, Contacte con soporte.
Olvidó guardar el secretoUse el ojo o el icono de copiar de la clave activa. El código de recuperación no es el secreto de API.
No aparece el acceso a cobros por APIContacte con soporte para revisar el acceso. El servicio de enlaces y el acceso a API pueden habilitarse por separado.

Resumen de endpoints

EndpointFinalidad
POST /api/payments/hostedCrear un pago con la Hosted API · Hosted API
GET /api/payments/{payment_id}Consultar un pago · Hosted API · Direct API
GET /api/payments/bin/{bin6}Comprobar el BIN (seis primeros dígitos de la tarjeta) · Direct API
POST /api/payments/directCrear un pago con la Direct API · Direct API
GET /api/account/balanceSaldo de la cuenta · Saldos
GET /api/account/available-balanceSaldo retirable · Saldos
POST|GET /api/account/refundsReembolsos y anulaciones · Reembolsos
POST|GET /api/account/withdrawalsRetiros en USDT · Retiros
GET /api/account/disputesDisputas y respuestas a disputas · Disputas
GET|POST /api/webhooks/deliveriesHistorial y reenvío de entregas de webhooks · Entregas
POST /api/sandbox/disputesCrear una disputa de prueba (solo con clave de sandbox) · Sandbox

Todos los endpoints utilizan la misma autenticación y cada POST requiere además un Idempotency-Key.

Autenticación

Cada solicitud incluye su clave de API y una firma HMAC-SHA256 calculada con su clave secreta.

Clave de API y clave secreta

ValorFormatoDescripción
Clave de APIapi_ + 32 hexSe envía en X-Api-Key. La clave también determina el entorno.
Clave secreta64 hexFirma solicitudes y verifica firmas de webhook; no se envía en la solicitud. El titular puede volver a ver o copiar un secreto activo con los iconos de ojo/copiar del panel.
Código de comercio[0-9]{6}Su número de cuenta, que se envía en X-Merchant-Code.

Cada clave de API pertenece exactamente a un entorno, sandbox o producción, y la clave determina el entorno en el que se ejecuta la solicitud: utilice la clave de sandbox para las pruebas y la clave de producción para los pagos reales. El encabezado X-Environment es opcional; si se omite, la solicitud se ejecuta en el entorno de la clave. Si se envía, su valor debe ser sandbox o live (sin distinguir entre mayúsculas y minúsculas) y debe coincidir con el entorno de la clave; de lo contrario, la solicitud se rechaza con HTTP 401 y el código 0055502, y no se procesa nada. Los encabezados de autenticación obligatorios son X-Api-Key, X-Merchant-Code, X-Timestamp y X-Signature, además de Idempotency-Key en cada solicitud de escritura. El entorno no forma parte de la cadena que se firma. Todo cuerpo de respuesta que contiene environment (pagos, saldos, reembolsos y otros recursos) indica el entorno utilizado.

Rotación de claves

  • Cree una clave nueva si la clave secreta puede haber quedado expuesta o cuando una persona con acceso a ella deje de formar parte de su equipo. La clave anterior deja de funcionar de inmediato.
  • Los webhooks y los enlaces de retorno de un pago se firman con la clave secreta asociada a la clave de API que creó el pago; esto también se aplica a los webhooks de reembolsos y disputas enviados mucho después del pago. Conserve la clave secreta anterior mientras puedan seguir llegando estos eventos.

Encabezados de la solicitud

EncabezadoObligatorioValor
X-Api-KeyObligatorioSu clave de API.
X-Merchant-CodeObligatorioSu código de comercio de seis dígitos.
X-TimestampObligatorioHora Unix actual en segundos (10 dígitos).
X-SignatureObligatorioLa firma de la solicitud: 64 caracteres hexadecimales en minúsculas.
Idempotency-KeyCondicionalObligatorio en cada POST. Consulte Idempotency-Key.
Content-TypeCondicionalapplication/json para las solicitudes con cuerpo.
Accept-LanguageOpcionalIdioma de los mensajes de error: en (predeterminado), tr, de, ar, es o el.

Envíe cada encabezado X- exactamente una vez. Los valores de los encabezados no deben contener comas, saltos de línea ni espacios.

Firma de las solicitudes

La cadena que se firma consta de cuatro valores unidos por saltos de línea (LF): la marca de tiempo, el método HTTP, la ruta y el hash SHA-256 del cuerpo.

Cadena que se firma

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. Codifique el cuerpo como JSON una sola vez y calcule el hash SHA-256 de exactamente esos bytes (hexadecimal en minúsculas). Para una solicitud sin cuerpo, calcule el hash de la cadena vacía (e3b0c442…b855).

  2. Utilice la ruta exactamente como aparece en la URL, empezando por /api/ y sin la cadena de consulta. Por ejemplo, GET /api/payments/bin/540061?amount=100.00 firma /api/payments/bin/540061.

  3. Calcule el HMAC-SHA256 con la clave secreta como clave HMAC. Utilice la cadena de la clave secreta tal cual; no la decodifique.

  4. Envíe el resultado en X-Signature y envíe como cuerpo de la solicitud exactamente los bytes sobre los que calculó el hash.

Tolerancia de la marca de tiempo

X-Timestamp debe estar dentro de un margen de 300 segundos respecto a la hora del servidor de Payniko, por delante o por detrás. Mantenga el reloj de su servidor sincronizado mediante NTP. Si la desviación es mayor, la solicitud se rechaza con 401 0055504 y detail contiene la hora del servidor de Payniko.

Ejemplo de firma

Utilice estos valores de ejemplo para probar su implementación. Con esta clave secreta, esta marca de tiempo y este cuerpo, su código debe generar exactamente esta firma.

Clave secreta (ejemplo)

e3c4a5b6d7e8f90112233445566778899aabbccddeeff0011223344556677889

Cuerpo (bytes exactos)

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

Cadena que se firma

1793534400
POST
/api/payments/hosted
dca2bae6e50c13c26b22b57e3c31161ba3174147060fcbf22b2ba99cc13560ae

X-Signature

5522c27ff0b49ffa4c44712ed5db350c1c5a2cbfc0c568edbce39cff59ca620c

Encabezados de la solicitud

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

Funciones auxiliares de solicitud

Añada una de estas funciones auxiliares a su servidor. La función lee API_KEY, API_SECRET y MERCHANT_CODE de las variables de entorno, firma la solicitud y la envía. Los demás ejemplos de código utilizan esta función auxiliar.

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

Verificación de la configuración

Consultar el saldo es una primera llamada segura, ya que no crea nada.

./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)

Las firmas no válidas repetidas bloquean temporalmente la clave: 20 fallos desde la misma dirección en 15 minutos la bloquean durante 15 minutos, y 300 fallos en una hora la bloquean durante una hora (429 1000005 con Retry-After). Corrija la lógica de firma antes de reintentar.

Idempotency-Key

Cada POST requiere un Idempotency-Key. Una solicitud reenviada con la misma clave se ejecuta una sola vez y devuelve el resultado original, lo que permite reintentar de forma segura tras un tiempo de espera agotado.

  • Genere una clave aleatoria nueva, como un UUID, para cada operación nueva y guárdela con su pedido antes de enviar la solicitud.
  • Tras un tiempo de espera agotado o un error de red, reenvíe el mismo cuerpo con la misma clave.
  • No reutilice nunca una clave con un cuerpo distinto; esa solicitud se rechaza con 409 (9000010, o 0055503 en la Direct API).
  • Si la solicitud original sigue en curso, la respuesta es 409: 9000039 en la Hosted API (con integration.retryable: true) o 0055503 con metadata.retry_class: retry_same_request en la Direct API. Reenvíe la misma solicitud tras una breve espera.
  • Formato: de 8 a 128 caracteres de A-Z a-z 0-9 _ -. Si la clave falta o no es válida, se devuelve 422 1000009.
  • Una solicitud firmada (X-Timestamp y X-Signature) queda vinculada durante 10 minutos a la primera Idempotency-Key con la que se envía; la misma firma con otra clave se rechaza con 409 9000010. Genere una marca de tiempo y una firma nuevas para cada operación nueva. Si esta comprobación no está disponible temporalmente, la respuesta es 503 1000011.

Requisitos de las solicitudes

  • Envíe únicamente los campos documentados. Los campos desconocidos se rechazan con 422 1000001.
  • Las solicitudes POST no deben incluir una cadena de consulta. Las solicitudes GET no deben incluir cuerpo y solo admiten los parámetros de consulta documentados.
  • El cuerpo debe ser un único objeto JSON con un máximo de 7 niveles de anidación, incluido el objeto de nivel superior.
  • Los límites de velocidad se aplican por dirección IP (600 solicitudes por minuto antes de la autenticación) y por clave (300 solicitudes por minuto). Ante un 429 (1000005), espere el intervalo Retry-After y reintente con espera progresiva. Cada respuesta incluye X-RateLimit-Limit y X-RateLimit-Remaining.
EncabezadoSignificado
X-Correlation-IdIdentificador único de la solicitud. Regístrelo e inclúyalo en sus consultas de soporte.
Retry-AfterNúmero de segundos que debe esperar. Siempre presente con 429; presente con 503 cuando se conoce.
X-RateLimit-LimitLímite de solicitudes de la ventana actual. No existe la cabecera X-RateLimit-Reset.
X-RateLimit-RemainingSolicitudes restantes en la ventana actual.
Content-LanguageIdioma de los mensajes de la respuesta.

Errores de autenticación

CódigoHTTPSignificadoAcción recomendada
0055502401 / 403Acceso denegado. 401: falta X-Api-Key o X-Merchant-Code, un encabezado está duplicado o tiene un formato no válido, la clave es desconocida o está revocada, el código de comercio no coincide, o se envió X-Environment y no coincide con la clave. 403: la cuenta o el acceso a la Direct API no están activos.Verifique los encabezados y la clave de API. No reintente la solicitud de forma automática.
0055504401X-Timestamp o X-Signature falta o tiene un formato no válido, o el reloj de su servidor difiere de la hora del servidor de Payniko en más de 300 segundos.Corrija el reloj de su servidor (NTP) y vuelva a firmar la solicitud con una marca de tiempo actual.
0055505401La firma no coincide con la solicitud.Calcule el hash de los bytes exactos del cuerpo que envía, firme la ruta sin la cadena de consulta y utilice la clave secreta correcta.
9000002401La solicitud ya había expirado en el momento de procesarse.Vuelva a firmar la solicitud con una marca de tiempo actual y reenvíela.
1000009422Falta el encabezado Idempotency-Key o no es válido.Envíe con cada POST una clave de 8 a 128 caracteres compuesta por A-Z a-z 0-9 _ -.
9000010409Este Idempotency-Key ya se utilizó para una solicitud distinta, o la misma solicitud firmada se reenvió con una clave diferente.Reintente con la misma clave y el mismo cuerpo; utilice una clave nueva solo para una operación nueva.
1000005429Demasiadas solicitudes, o bloqueo temporal tras fallos de firma repetidos.Espere el intervalo indicado en Retry-After y, a continuación, reintente con esperas crecientes.

Hosted API

Su servidor crea el pago y redirige al pagador a la página de pago de Payniko. Payniko recopila los datos de la tarjeta y realiza la autenticación 3-D Secure, por lo que los datos de la tarjeta nunca llegan a sus sistemas. La Hosted API es la opción de integración más sencilla.

Pasos de integración

  1. Cree el pago

    Su servidor llama a POST /api/payments/hosted. Guarde el payment_id devuelto junto con su pedido.

  2. Redirija al pagador

    Redirija el navegador a next_action.url o ábrala en una pestaña nueva; no la cargue nunca dentro de un iframe.

  3. El pagador completa el pago

    El pagador introduce los datos de la tarjeta. Si el banco exige 3-D Secure, la página lo presenta. Un rechazo intermedio en esta fase no es definitivo; el pago alcanza un estado final sin que usted tenga que intervenir.

  4. El pagador regresa

    La página muestra el resultado con una breve cuenta atrás y, a continuación, redirige el navegador con parámetros firmados a success_url (solo si el pago está en succeeded) o a fail_url. Consulte URL de retorno.

  5. Confirme el resultado

    Espere el webhook o llame a GET /api/payments/{payment_id}. Entregue el pedido solo cuando el estado sea succeeded.

Flujo de pago

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/hostedRequiere Idempotency-Key

Crear un pago

Devuelve 201 con el pago nuevo. Si se reenvía la misma solicitud con el mismo Idempotency-Key, se devuelve 200 con el mismo pago.

Cuerpo de la solicitud

CampoTipoObligatorioDescripciónEjemplo
merchant_order_idstringObligatorioSu referencia de pedido: de 1 a 64 caracteres de A-Z a-z 0-9 . _ -. Cada referencia puede utilizarse para un solo pago por entorno, aunque ese pago haya fallado o expirado. La unicidad no distingue entre mayúsculas y minúsculas: ORDER-1 y order-1 se consideran la misma referencia.ORDER-2026-001
amountstringObligatorioImporte positivo con dos decimales y sin ceros a la izquierda, por ejemplo "100.00". Debe estar dentro de los límites de su cuenta."100.00"
currencystringOpcionalSi se omite, se aplica la moneda principal del sistema.
USDEURAEDTRY
USD
webhook_urlstring · uriObligatorioPunto de conexión que recibe los webhooks de este pago. HTTPS público en el puerto 443, 500 caracteres como máximo, sin información de usuario ni fragmento. No se aceptan direcciones privadas ni locales, tampoco en sandbox; el nombre de host debe resolverse a una IP pública en el momento de la solicitud, de lo contrario 422 9000008.https://merchant.example/webhooks/payments
success_urlstring · uriObligatorioDestino de redirección del navegador tras un pago completado correctamente. HTTPS, 2048 caracteres como máximo, sin información de usuario ni fragmento. En sandbox también se admiten http://localhost, 127.0.0.1 y [::1].https://merchant.example/payment/success
fail_urlstring · uriObligatorioDestino de redirección del navegador cuando el pago falla, expira o se cancela. Se aplican las mismas reglas que a success_url.https://merchant.example/payment/failure
customerobjectObligatorioDatos de contacto del pagador; se usan para el resultado del pago, el recibo y los pasos de verificación del banco. Solo se aceptan email y phone; cualquier otro campo dentro de customer se rechaza con 422 1000001.{ … }
customer.emailstringObligatorioLa dirección de correo electrónico real del pagador: RFC 5322, 254 caracteres como máximo, con dominio con punto. Se guarda en minúsculas.[email protected]
customer.phonestringObligatorioEl número de teléfono real del pagador en formato internacional (+ código de país, E.164), por ejemplo +37061234567. Regla específica para Turquía: +90 seguido del número nacional de 10 dígitos (5xx móvil, 2xx, 3xx, 4xx fijo, 8xx especial). Un número enviado sin código de país se trata como número turco (+90).+37061234567

Estas tres URL se aplican únicamente a este pago y no se toman de la configuración de su panel.

La página de pago no vuelve a pedir al pagador el correo electrónico ni el número de teléfono. Por privacidad, customer no se devuelve en el objeto de pago ni en los webhooks.

CampoMotivoMensaje en errors
customer.emailFormato no válido o más de 254 caracteres.customer.email must be a valid e-mail address (max 254 characters).
customer.emailDirección de relleno, de ejemplo o desechable.customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted.
customer.phoneNo es un número válido en formato internacional.customer.phone must be a valid phone number in international format (e.g. +37061234567).
customer.phonePatrón de dígitos repetidos o consecutivos.customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted.
customer.phoneNúmero turco que no tiene la forma +90 seguido de 10 dígitos que empiezan por 2, 3, 4, 5 u 8.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."
    ]
  }
}

Si una solicitud se rechaza por customer, muestre al pagador el motivo con claridad (por ejemplo: introduzca su propio correo electrónico o número de teléfono) y permítale corregir el campo antes de crear el pago de nuevo. Los mensajes de errors están en inglés y están pensados para sus registros y para soporte.

Las páginas de enlace de pago de Payniko aplican las mismas reglas al correo electrónico y al número de teléfono que el pagador introduce allí, y le muestran el motivo de un rechazo.

Cuerpo de la solicitud · Ejemplo

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

Respuesta · 201

Devuelve { "data": Payment } con status: requires_action y next_action.url.

Respuesta · Ejemplo

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

Otras respuestas

  • 200: se reenviaron la misma clave y el mismo cuerpo. Se devuelve el pago original y no se crea ningún pago nuevo.
  • 409 9000010: esta clave ya se utilizó con un cuerpo distinto.
  • 409 9000039: la primera solicitud con esta clave todavía se está procesando. Reenvíe la misma solicitud tras una breve espera.
  • 409 9000007: ya existe un pago para este merchant_order_id.
  • 422 9000005: el importe está fuera de los límites de su cuenta; limits contiene el rango permitido.
  • 422 1000001 con customer, customer.email o customer.phone en errors: faltan los datos de contacto o no son válidos.
  • 422 1000001, 1000020, 1000021, 9000008, 9000038: un campo no es válido; los detalles figuran en errors y en 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
  }
}

Los límites se evalúan en la moneda principal del sistema (limits.currency_code) después de convertir su importe. max_amount: null significa que no hay límite superior. Los valores de límite de este ejemplo son ilustrativos.

Objeto de pago

Las llamadas de creación y consulta devuelven este objeto dentro de data, y todos los webhooks de pago lo contienen. Su estructura es idéntica en la Hosted API y en la Direct API.

CampoTipoDescripción
payment_idstring · LUVEI-ABCD-123456ID de Payniko del pago. Guárdelo junto con su pedido.
merchant_order_idstringSu referencia de pedido, exactamente como la envió.
flowstringhosted para la Hosted API, direct para la Direct API.
hosteddirect
statusstringEstado actual del pago; consulte la tabla de estados más abajo.
requires_actionprocessingsucceededfailedexpiredcancelled
amountstringImporte con dos decimales.
currencystringMoneda del importe; la moneda principal del sistema si no se especificó ninguna.
environmentstringEntorno de la clave que creó el pago.
sandboxlive
next_actionobject | nullLa siguiente acción que debe realizar el pagador. Solo está presente mientras status es requires_action.
next_action.typestringSiempre redirect.
next_action.urlstring · uriAbra esta URL en el navegador del pagador exactamente como se devuelve. No la construya ni la modifique nunca.
next_action.expires_atstring · date-timePlazo para utilizar la URL. Con la Direct API, corresponde al final del paso de 3-D Secure y puede ser anterior al expires_at del propio pago; con la Hosted API, es el plazo del pago. Una vez vencido, el pago pasa a su siguiente estado y next_action pasa a ser null.
cardobject | nullDatos enmascarados de la tarjeta, una vez conocidos.
card.bin6string | nullSeis primeros dígitos.
card.last_fourstring | nullCuatro últimos dígitos.
card.networkstring | nullRed de tarjetas.
visamastercardamextroyunionpaydiscoverjcbdinersmaestro
card.bankstring | nullBanco emisor, si se conoce.
failureobject | nullMotivo del fallo. Solo está presente cuando status es failed.
failure.codestringCódigo de motivo para utilizar en su lógica; consulte la lista más abajo.
failure.messagestringMensaje breve apto para mostrarse al pagador como texto sin formato.
created_atstring · date-timeFecha y hora de creación del pago.
expires_atstring · date-timePlazo para completar el pago.
completed_atstring · date-time | nullFecha y hora en que el pago alcanzó un estado final.

next_action puede ser null brevemente incluso en requires_action; en ese caso, vuelva a consultar el pago. Nunca se devuelven el número completo de la tarjeta, el CVV, la fecha de vencimiento ni el nombre del titular. card.network y card.bank pueden ser null cuando no se conocen.

Los datos de customer enviados al crear el pago no forman parte de este objeto y no aparecen en los webhooks.

Pago · Ejemplo

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

Estados de los pagos Hosted

EstadoSignificadoAcción recomendada
requires_actionEl pago se ha creado y el pagador aún no lo ha completado.Redirija al pagador a next_action.url (hasta next_action.expires_at).
processingEl pagador ha pagado y la respuesta del banco está pendiente.Espere el webhook y no cree un nuevo pago. Si el banco nunca responde, el pago puede permanecer en processing hasta la conciliación y después termina como failed o expired.
succeededLos fondos se han cobrado.Entregue el pedido.
failedEl pago no se completó. failure contiene el motivo.Muestre failure.message. Para reintentar, cree un pago nuevo con un merchant_order_id nuevo.
expiredEl pagador no completó el pago en un plazo de 24 horas (expires_at). Si el pagador abandona el pago a mitad del proceso, este puede finalizar como failed en lugar de expirar.Si el pagador aún desea pagar, cree un pago nuevo.
cancelledEl pago se canceló.No entregue el pedido.
  • Un rechazo intermedio en la página de pago no es definitivo: el pago puede permanecer en requires_action y es posible que reciba payment.updated. Finalmente, recibirá un único resultado final.
  • succeeded, failed, expired y cancelled son estados finales. Un pago en processing aún puede completarse correctamente después de expires_at.
  • Trate cualquier estado que no figure aquí como processing y vuelva a consultar el pago.

Códigos de fallo

Un pago en estado failed contiene failure.code y failure.message. Utilice el código en su lógica y muestre el mensaje al pagador como texto sin formato.

CódigoSignificado
payment_declinedEl banco rechazó el pago.
do_not_honorEl banco rechazó el pago sin indicar el motivo.
insufficient_fundsLa tarjeta no tiene fondos suficientes.
card_limit_reachedSe ha alcanzado un límite de la tarjeta.
card_details_invalidLos datos de la tarjeta no son válidos.
card_expiry_invalidLa fecha de vencimiento no es válida o la tarjeta ha vencido.
card_security_code_invalidEl CVV no es válido.
card_not_activeLa tarjeta está bloqueada o no está activa.
card_brand_not_supportedEsta red de tarjetas no está admitida.
three_ds_failedLa autenticación 3-D Secure falló o no se completó.
payment_not_permittedLa tarjeta no está habilitada para este tipo de pago.
amount_not_allowedEl importe no está permitido para esta tarjeta.
currency_not_supportedLa moneda no está admitida para esta tarjeta.
payment_details_rejectedEl banco rechazó los datos del pago.
duplicate_paymentEl banco identificó el pago como duplicado.
payment_timed_outEl pago no se completó dentro del plazo.
bank_unavailableNo fue posible comunicarse con el banco.
bank_requested_retryEl banco solicitó que se reintente el pago más tarde.
service_unavailableNo fue posible procesar el pago en este momento.

Es posible que se añadan códigos nuevos; trate cualquier código desconocido como payment_declined. failure.message puede contener el texto del propio banco; de lo contrario, está en inglés en los webhooks y se ajusta a Accept-Language en GET.

Requisitos de redirección

  • Redirija la página completa (303 o window.location) a next_action.url o ábrala en una pestaña nueva. No la inserte en un iframe; la página bloquea la carga dentro de marcos.
  • Utilice la URL antes de next_action.expires_at. Una vez que haya expirado, consulte el pago.
  • El idioma de la página se ajusta a la configuración del navegador del pagador; no se requiere ningún parámetro.

URL de retorno

Cuando el pagador regresa, el navegador se redirige a success_url solo si el pago está en succeeded, y a fail_url en todos los demás casos. A la URL se añaden parámetros firmados. Verifíquelos para mostrar la página correcta y entregue el pedido solo después de que el webhook o un GET confirmen succeeded.

CampoDescripción
payment_idEl ID del pago .
merchant_order_idSu referencia de pedido. No está firmada: utilícela solo para localizar el pedido y realice la correspondencia mediante payment_id.
statusEstado del pago en el momento de la redirección, con los mismos valores que en GET. Siempre succeeded en success_url. En fail_url, normalmente failed, expired o cancelled; con la Hosted API puede ser requires_action o processing si el pagador se fue antes de que el pago alcanzara un estado final, por ejemplo mediante "Volver a la tienda".
failure_codeSolo está presente cuando status es failed; idéntico a failure.code.
tsHora Unix en la que se creó el enlace de retorno. El pagador puede abrir el enlace más tarde, por lo que un ts antiguo no indica por sí solo una falsificación.
key_versionIdentifica la clave que firmó el enlace: 1 para la primera clave creada en ese entorno, 2 para la siguiente, y así sucesivamente. El valor nunca cambia para un mismo pago.
sigHMAC-SHA256 en hexadecimal en minúsculas, de 64 caracteres.

Cadena que se firma

ts + "." + payment_id + "." + status + "." + failure_code   // failure_code empty: keep the last dot
  1. Seleccione la clave secreta correspondiente a la clave indicada en key_version. Es la clave que creó el pago, aunque las claves se hayan sustituido desde entonces.

  2. Construya la cadena con los valores decodificados de la URL. Si failure_code no está presente, mantenga el punto al final de la cadena.

  3. Calcule el HMAC-SHA256 y compárelo con sig en tiempo constante. Confirme que payment_id corresponde a uno de sus pagos.

  4. Consulte el pago con GET /api/payments/{payment_id} (o espere el webhook) antes de entregar el pedido.

  • Sus propios parámetros de consulta se conservan. Si la URL ya contiene un parámetro con alguno de los nombres anteriores, se sustituye.
  • Si el enlace no pudo firmarse debido a un error temporal, la URL no contiene ninguno de estos parámetros. Considere el resultado como desconocido y consulte el pago.
// 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.

URL de retorno · Ejemplo

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}

Consultar un pago

Devuelve { "data": Payment } con el estado actual. La consulta de un pago nunca lo modifica, por lo que puede llamar a este endpoint tantas veces como sea necesario: tras el retorno, cuando un webhook parezca incoherente o tras un tiempo de espera agotado.

Parámetros

CampoTipoObligatorioDescripción
payment_idstring · pathObligatorioEl payment_id devuelto al crear el pago. Cualquier clave del mismo entorno puede consultarlo.
./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)

Posibles errores

Webhooks de pago

Los siguientes eventos se envían a la webhook_url del pago. Cada evento de pago se envía como máximo una vez por pago.

EventoSignificado
payment.succeededEl estado del pago es succeeded. Entregue el pedido.
payment.failedEl pago finalizó sin éxito: failed, expired o cancelled. Consulte data.status.
payment.updatedTodavía se requiere una acción del pagador (requires_action), por ejemplo tras un rechazo intermedio. No es un resultado final.
refund.* · payment.voided · dispute.*Eventos posteriores de este pago (reembolsos, anulaciones, disputas). Consulte 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"
  }
}

Verifique cada webhook: el HMAC-SHA256 de X-Timestamp + "." + raw body, calculado con la clave secreta asociada a la clave de API que creó el pago, debe ser igual a X-Signature, y X-Timestamp debe estar dentro de un margen de 300 segundos. Almacene cada event_id una sola vez y responda con 2xx sin demora. Para ver ejemplos de código y el comportamiento de los reintentos, consulte Webhooks.

Errores

CódigoHTTPSignificadoAcción recomendada
1000001422La solicitud no es válida: un campo desconocido, un formato no válido, una cadena de consulta en un POST, un parámetro de consulta no admitido o un cuerpo que no es un objeto JSON. errors enumera los campos afectados.Corrija los campos indicados.
1000009422Falta el encabezado Idempotency-Key o no es válido.Envíe con cada POST una clave de 8 a 128 caracteres compuesta por A-Z a-z 0-9 _ -.
9000010409Este Idempotency-Key ya se utilizó para una solicitud distinta, o la misma solicitud firmada se reenvió con una clave diferente.Reintente con la misma clave y el mismo cuerpo; utilice una clave nueva solo para una operación nueva.
9000039409La primera solicitud con esta Idempotency-Key todavía se está procesando (Hosted API).Reenvíe la misma solicitud tras una breve espera.
9000007409Ya existe un pago para este merchant_order_id (Hosted API).Consulte el pago existente o utilice una nueva referencia de pedido.
9000005422El importe está fuera de los límites de su cuenta (Hosted API). limits contiene el rango permitido.Ajuste el importe.
1000020422El formato del importe no es válido.Envíe una cadena que represente un importe positivo con dos decimales y sin ceros a la izquierda, por ejemplo "100.00".
1000021422La moneda no está admitida.Utilice USD, EUR, AED o TRY, u omita currency.
9000008422El valor de webhook_url no es válido.Utilice una URL HTTPS pública en el puerto 443 con una longitud máxima de 500 caracteres.
9000038422El valor de success_url o fail_url no es válido.Utilice una URL HTTPS con una longitud máxima de 2048 caracteres. En sandbox también se admite http://localhost.
9000040403La Hosted API no está habilitada para su cuenta.Póngase en contacto con soporte.
9000013409La operación de sandbox no está disponible; por ejemplo, cuando se llama a un endpoint exclusivo de sandbox con una clave de producción.Utilice una clave de sandbox o reintente más tarde.
9000004403Esta operación está restringida para su cuenta.Póngase en contacto con soporte.
3000001503El tipo de cambio no está disponible temporalmente.Reintente más tarde.
1000011503El servicio no está disponible temporalmente.Reintente la misma solicitud. Consulte los recursos existentes antes de crear otros nuevos.
1000013503Esta función aún no está disponible.Reintente transcurridos retry_after segundos.
1000014503Hay un mantenimiento programado en curso. until y note proporcionan los detalles.Reintente una vez transcurrido el intervalo indicado en Retry-After.

El rechazo de una tarjeta no es un error HTTP; el pago finaliza como failed con failure.code. Para ver la lista completa, consulte Códigos de error.

Pruebas con tarjetas de sandbox

Cree el pago con su clave de sandbox, abra next_action.url y pague con una de las tarjetas siguientes, con cualquier fecha de vencimiento futura y cualquier CVV de 3 dígitos. Para más información, consulte Sandbox y tarjetas de prueba.

Número de tarjetaResultado
4000000000000010Aprobado sin 3-D Secure. Con la Direct API, la respuesta de creación ya devuelve succeeded.
4000000000000002 · 4355090000044242Paso de 3-D Secure de prueba, seguido de succeeded.
4000000000000051 · 4355090000040000Rechazada antes de 3-D Secure (do_not_honor). Hosted API: el pago puede permanecer unos 5 minutos en requires_action y enviar payment.updated antes de terminar como failed. Direct API: la respuesta de creación ya devuelve failed.
4000000000000093 · 4355090000005151Paso de 3-D Secure de prueba, seguido de failed con insufficient_funds.
4000000000000044Sin respuesta del banco ni 3-D Secure; el pago permanece en processing.
4355090000089999Hosted API: processing sin paso 3-D Secure. Direct API: paso 3-D Secure de prueba y después processing.

Ejemplo completo

Un servidor Node.js mínimo que crea un pago, redirige al pagador y gestiona el retorno y el webhook. Utiliza la función auxiliar de solicitud y las funciones de verificación de Webhooks y URL de retorno.

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 envía un POST firmado a su webhook_url cuando cambia el estado de un pago, un reembolso, una disputa o un retiro; el primer intento suele realizarse en el minuto siguiente al evento. Verifique la firma, guarde el evento y responda con 2xx antes de procesarlo.

Eventos

EventoSignificado
payment.succeededEl estado del pago es succeeded. Entregue el pedido.
payment.failedEl pago finalizó sin éxito: failed, expired o cancelled. Consulte data.status.
payment.updatedTodavía se requiere una acción del pagador (requires_action), por ejemplo tras un rechazo intermedio. No es un resultado final.
refund.completedSe ha completado un reembolso.
payment.voidedSe ha completado una anulación.
refund.rejectedSe rechazó un reembolso o una anulación.
refund.failedFalló un reembolso o una anulación.
refund.cancelledSe canceló un reembolso o una anulación.
dispute.openedSe abrió una disputa.
dispute.in_defenseLa disputa está en revisión.
dispute.wonLa disputa se resolvió a su favor.
dispute.lostLa disputa se resolvió en su contra.
withdrawal.sentSe envió un retiro.
withdrawal.rejectedSe rechazó un retiro.

Los eventos de pagos, reembolsos y disputas se envían a la webhook_url del pago, y los eventos de retiros, a la webhook_url del retiro. Cada evento de pago se envía como máximo una vez por pago; cada evento de reembolso, disputa o retiro, como máximo una vez por reembolso, disputa o retiro y por estado.

Carga útil

CampoTipoDescripción
event_idstring · uuidID único del evento; idéntico al encabezado X-Event-Id.
event_typestringNombre del evento, tal como figura en la tabla anterior.
environmentstringsandbox o live.
occurred_atstring · date-timeFecha y hora en que se produjo el evento.
subjectobjectSolo está presente en los eventos de reembolsos, disputas y retiros: el type y el id del reembolso, la disputa o el retiro.
dataobjectEn los eventos de pago, el objeto de pago (consulte Hosted API); en los demás eventos, los campos que se indican a continuación.

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

Eventos de reembolsos, disputas y retiros

EntidadCampo
refundid, status, operation_type, amount, currency, payment_id o payment_code, merchant_order_id
disputeid, status, amount, currency, payment_id o payment_code, merchant_order_id, reason, evidence_due_at (si está definido)
withdrawalid, status, amount, currency, usdt_amount, asset, network, tx_hash
  • Los eventos de reembolsos y disputas contienen una única referencia al pago: payment_id o, en algunos eventos, payment_code. refund.completed siempre incluye payment_id. Cuando un evento contiene payment_code, identifique el pedido mediante merchant_order_id.
  • Antes de actuar en función de un evento, consulte el reembolso, la disputa o el retiro con GET para confirmar su estado actual.

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

Verificación de la firma

EncabezadoValor
X-Event-IdIdéntico a event_id.
X-Environmentsandbox o live. Utilice la clave secreta de ese entorno.
X-TimestampHora Unix de este intento de entrega.
X-SignatureHMAC-SHA256 en hexadecimal en minúsculas de X-Timestamp + "." + raw body.
  1. Lea los bytes sin procesar del cuerpo antes de analizar el JSON.

  2. Verifique que X-Timestamp tiene 10 dígitos y está dentro de un margen de 300 segundos respecto a la hora de su servidor.

  3. Calcule el HMAC-SHA256 de X-Timestamp + "." + raw body con su clave secreta y compare el resultado con X-Signature en tiempo constante.

  4. Si la verificación falla, rechace la solicitud con 401.

// 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

Procesamiento único de cada evento

  1. Almacene event_id en una tabla con un índice único. Si ya existe, responda con 2xx y detenga el procesamiento.

  2. Responda con 2xx en pocos segundos y realice posteriormente, en una cola, las tareas que requieran más tiempo (correos electrónicos, actualizaciones de inventario).

  3. Los eventos pueden llegar con retraso, más de una vez o desordenados. Un pago que finaliza por tiempo de espera agotado puede notificarse un minuto o más después. Base sus decisiones en el estado incluido en data o vuelva a consultar el objeto con GET.

  4. No entregue nunca un pedido solo porque el navegador haya regresado a success_url.

Reintentos

  • Cualquier respuesta 2xx recibida en unos 10 segundos cuenta como entrega correcta. Las redirecciones no se siguen y cuentan como fallos.
  • Las entregas fallidas se reintentan al cabo de aproximadamente 1 minuto, 5 minutos, 15 minutos, 1 hora y 6 horas. El cuerpo y X-Event-Id no cambian; X-Timestamp y X-Signature se generan de nuevo en cada intento.
  • Tras el último intento, la entrega se marca como failed. Algunos errores (un destino con IP privada, una clave de firma inexistente) no se reintentan y la entrega se marca como failed de inmediato. Las entregas fallidas pueden reenviarse con la API de entregas.
  • Su webhook_url debe resolverse en una dirección IP pública en cada intento.

Entregas de webhooks

Cada webhook enviado se almacena como una entrega. Utilice esta API para identificar las entregas fallidas y reenviarlas.

GET/api/webhooks/deliveries

Listar y consultar entregas

./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)

Filtre por status (pending, delivered, failed) y pagine con page y per_page. La lista contiene las entregas del entorno de su clave. GET /api/webhooks/deliveries/{id} devuelve además url y payload.

CampoTipoDescripción
idstring · uuidID de la entrega; idéntico al event_id del webhook.
eventstringNombre del evento, por ejemplo payment.succeeded.
statusstringEstado de la entrega.
pendingdeliveredfailed
payment_idstring · LUVEI-ABCD-123456 | nullPago relacionado, si existe.
subject_typestring | nullrefund, dispute o withdrawal; null en los eventos de pago.
subject_idstring | nullID del reembolso, la disputa o el retiro.
attempt_countintegerNúmero de intentos realizados hasta el momento.
last_status_codeinteger | nullCódigo de estado HTTP devuelto por su endpoint en el último intento.
last_error_codestring | nullMotivo del fallo del último intento; consulte la tabla más abajo.
last_error_messagestring | nullDescripción legible de last_error_code.
last_errorstring | nullError en texto libre, que puede estar en turco. Utilice last_error_code en su lugar.
next_attempt_atstring · date-time | nullFecha y hora del próximo intento automático.
delivered_atstring · date-time | nullFecha y hora en que su endpoint aceptó la entrega.
created_atstring · date-timeFecha y hora de creación del webhook.
urlstringSolo en la respuesta de detalle: la URL de destino.
payloadobject | nullSolo en la respuesta de detalle: el cuerpo almacenado del webhook, recodificado (las referencias al pago se muestran como payment_id). No coincide con los bytes firmados; no verifique firmas a partir de él.

Respuesta · Ejemplo

{
  "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"
}
CódigoSignificado
webhook_response_unsuccessfulSu endpoint respondió con un código de estado distinto de 2xx (consulte last_status_code) o con una redirección.
webhook_unreachableNo fue posible conectar con su endpoint (DNS, conexión o tiempo de espera agotado).
invalid_webhook_targetLa URL almacenada no es una URL de webhook válida.
webhook_target_private_networkLa URL se resolvió en una dirección IP privada o local.
webhook_environment_invalidNo se pudo determinar el entorno del webhook.
webhook_signing_unavailableNo se pudo firmar el webhook.
webhook_egress_proxy_unreachableIncidencia temporal de entrega por parte de Payniko.
webhook_delivery_failedCualquier otro error de entrega.
POST/api/webhooks/deliveries/{id}/retryRequiere Idempotency-Key

Reenviar una entrega fallida

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)

Respuesta · 200

Devuelve la entrega con status: pending.

  • Solo se pueden reenviar las entregas en failed; para cualquier otra entrega se devuelve 409 9000025.
  • El mismo webhook se pone de nuevo en cola con el mismo id, el mismo event_id y el mismo cuerpo, y comienza una nueva serie de intentos.
  • Envíe un cuerpo vacío o {}.

Posibles errores

Sandbox y tarjetas de prueba

Pruebe su integración con una clave de sandbox antes de pasar a producción. El sandbox ofrece la misma API y los mismos webhooks que el entorno de producción, sin fondos reales ni procesamiento bancario real.

Funcionamiento del sandbox

  • No existe un selector de entorno: las solicitudes firmadas con una clave de sandbox se ejecutan en sandbox, y las firmadas con una clave de producción, en producción.
  • Las tarjetas siguientes funcionan tanto en la página de pago como en su propio formulario de pago; las tarjetas reales se rechazan. Su propio formulario también acepta cualquier otro número de tarjeta válido con BIN 435509 o 400000; en ese caso, una página de prueba le permite elegir éxito, fallo o ausencia de respuesta.
  • Las pantallas de 3-D Secure son de prueba y los números de tarjeta deben tener un dígito de control válido.
  • Los webhooks se entregan como solicitudes HTTPS reales a su webhook_url y se firman con su clave secreta de sandbox.

Tarjetas de prueba

Las tarjetas se comportan igual con la Hosted API y la Direct API salvo que la tabla indique lo contrario. Use cualquier fecha de caducidad futura y cualquier CVV de 3 dígitos.

Número de tarjetaResultado
4000000000000010Aprobado sin 3-D Secure. Con la Direct API, la respuesta de creación ya devuelve succeeded.
4000000000000002 · 4355090000044242Paso de 3-D Secure de prueba, seguido de succeeded.
4000000000000051 · 4355090000040000Rechazada antes de 3-D Secure (do_not_honor). Hosted API: el pago puede permanecer unos 5 minutos en requires_action y enviar payment.updated antes de terminar como failed. Direct API: la respuesta de creación ya devuelve failed.
4000000000000093 · 4355090000005151Paso de 3-D Secure de prueba, seguido de failed con insufficient_funds.
4000000000000044Sin respuesta del banco ni 3-D Secure; el pago permanece en processing.
4355090000089999Hosted API: processing sin paso 3-D Secure. Direct API: paso 3-D Secure de prueba y después processing.
  • Con cualquier tarjeta de prueba, un importe que termine en .77 (por ejemplo, 100.77) mantiene el pago en processing (revisión manual).
  • Un pago de prueba que nunca recibe respuesta se cierra finalmente como failed con service_unavailable.
  • Un pago con una tarjeta de prueba rechazada (por ejemplo, 4000000000000051) puede permanecer en requires_action durante un tiempo antes de alcanzar un estado final. Base sus decisiones en el webhook o en un GET, no en un rechazo intermedio.

BIN de prueba

En sandbox, la comprobación de BIN devuelve los siguientes resultados:

BINResultado
435509 · 400000200 con payment_mode: direct.
540061200 con payment_mode: hosted; el pagador continúa en la página de pago de sandbox.
979210409 con el código 0055230.
Cualquier otra combinación de seis dígitos409 con el código 0055233.

Reembolsos y retiros

En sandbox, los reembolsos y los retiros se completan de inmediato contra un saldo de prueba. Elija el resultado con la cabecera X-Sandbox-Scenario, que no forma parte de la firma. Use una Idempotency-Key nueva para cada escenario: la cabecera forma parte de la huella de idempotencia y la misma clave con otro escenario se rechaza con 409 9000010. Un valor desconocido, o la cabecera enviada con una clave live, devuelve 422 1000001; el punto de conexión de pruebas no acepta la cabecera. El escenario aplicado se devuelve en la cabecera de respuesta X-Sandbox-Scenario.

Antes de aplicar el escenario, sandbox impone las mismas reglas que live: límites mínimo y diario de retiro, tipos de cambio (503 3000001 si no están disponibles) y validación de la dirección. Cualquier dirección TRC20 con suma de verificación válida se acepta en sandbox, incluida la dirección cero de TRON, y no se realiza ninguna transferencia; 1000030 (una dirección que no puede recibir pagos) no se puede reproducir en sandbox.

EndpointValor
POST /api/account/refundscompleted (predeterminado) o rejected
POST /api/account/withdrawalssent (predeterminado) o 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)

El tx_hash de sandbox es simulado y no corresponde a una transacción real en la cadena de bloques.

POST/api/sandbox/disputesRequiere Idempotency-KeySolo con clave de sandbox

Crear una disputa de prueba

Abre una disputa sobre un pago de sandbox completado correctamente para que pueda probar la gestión de disputas y los webhooks. Disponible únicamente con una clave de sandbox; con una clave de producción se devuelve 409 9000013.

CampoTipoObligatorioDescripción
payment_idstring · LUVEI-ABCD-123456ObligatorioUno de sus pagos de sandbox en estado succeeded.
reasonstringObligatorioMotivo de la disputa.
fraudunauthorizedproduct_not_deliveredduplicateamount_mismatchother
scenariostringOpcionalEstado inicial. Valor predeterminado: 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)

Respuesta · 201

Devuelve el objeto de disputa. Una disputa de prueba en open admite respuestas durante 7 días; won y lost son finales de inmediato. El estado under_review no se produce en sandbox y las disputas de prueba no afectan a su saldo.

Registro de solicitudes

La página Registro de solicitudes enumera sus solicitudes recientes a la API con el estado, el código de error y el correlation_id. Las solicitudes rechazadas antes de identificar la clave (clave, firma o código de comercio no válidos) no se incluyen. Los pagos de sandbox se enumeran en Pedidos API de Sandbox.

Códigos de error

Los errores se devuelven como application/problem+json. Base su lógica en el código de estado HTTP y en el code de 7 dígitos; title y detail están destinados a lectores humanos.

Formato de la respuesta de error

CampoDescripción
typeURL del tipo de error: https://payniko.com/problems/{code}.
titleResumen breve del error.
statusCódigo de estado HTTP.
detailExplicación del error en esta solicitud.
codeCódigo estable de 7 dígitos; utilícelo en su lógica.
correlation_idIdéntico al encabezado X-Correlation-Id. Inclúyalo en sus consultas de soporte.
errorsMensajes por campo en los errores de validación: { "field": ["message"] }.
integrationIndicaciones para la integración: stage, reason, violations (location, path, expected, hint), docs_url, retryable y, en algunos casos, retry_after.
metadataPuede estar presente en los errores de la Direct API y en los errores de autenticación y de límite de frecuencia, con retry_class y un developer_detail informativo. Trátelo como opcional.
limitsRango de importes permitido en 9000005. En 0055235, el campo equivalente es amount_limit.
retry_afterNúmero de segundos que debe esperar; presente en algunos errores 503.

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

Pautas de reintento

retry_classSignificado
no_retryCorrija la solicitud antes de volver a enviarla.
retry_same_requestReenvíe la solicitud idéntica tras una breve espera (misma clave, mismo cuerpo).
retry_afterEspere el intervalo indicado en Retry-After y, a continuación, reenvíe la solicitud.
read_orderConsulte el pago con GET antes de decidir cómo proceder.
  • retry_class solo está presente en las respuestas que incluyen metadata (Direct API, errores de autenticación y de límite de velocidad). Los demás errores incluyen integration.retryable en su lugar.
  • Tras un tiempo de espera agotado o un 5xx, no cree nunca un pago nuevo; consulte el pago o reenvíe la solicitud con la misma clave.
  • El rechazo de una tarjeta no es un error HTTP: el pago se devuelve con 200, status: failed y failure.
  • developer_detail puede cambiar; no base en él la lógica de su integración.

Todos los códigos de error

Autenticación y acceso

CódigoHTTPSignificadoAcción recomendada
0055502401 / 403Acceso denegado. 401: falta X-Api-Key o X-Merchant-Code, un encabezado está duplicado o tiene un formato no válido, la clave es desconocida o está revocada, el código de comercio no coincide, o se envió X-Environment y no coincide con la clave. 403: la cuenta o el acceso a la Direct API no están activos.Verifique los encabezados y la clave de API. No reintente la solicitud de forma automática.
0055504401X-Timestamp o X-Signature falta o tiene un formato no válido, o el reloj de su servidor difiere de la hora del servidor de Payniko en más de 300 segundos.Corrija el reloj de su servidor (NTP) y vuelva a firmar la solicitud con una marca de tiempo actual.
0055505401La firma no coincide con la solicitud.Calcule el hash de los bytes exactos del cuerpo que envía, firme la ruta sin la cadena de consulta y utilice la clave secreta correcta.
9000002401La solicitud ya había expirado en el momento de procesarse.Vuelva a firmar la solicitud con una marca de tiempo actual y reenvíela.
9000040403La Hosted API no está habilitada para su cuenta.Póngase en contacto con soporte.
1000004403La creación de pagos nuevos está pausada temporalmente.Reintente más tarde.
9000004403Esta operación está restringida para su cuenta.Póngase en contacto con soporte.

Idempotencia y conflictos

CódigoHTTPSignificadoAcción recomendada
1000009422Falta el encabezado Idempotency-Key o no es válido.Envíe con cada POST una clave de 8 a 128 caracteres compuesta por A-Z a-z 0-9 _ -.
9000010409Este Idempotency-Key ya se utilizó para una solicitud distinta, o la misma solicitud firmada se reenvió con una clave diferente.Reintente con la misma clave y el mismo cuerpo; utilice una clave nueva solo para una operación nueva.
9000039409La primera solicitud con esta Idempotency-Key todavía se está procesando (Hosted API).Reenvíe la misma solicitud tras una breve espera.
9000007409Ya existe un pago para este merchant_order_id (Hosted API).Consulte el pago existente o utilice una nueva referencia de pedido.
0055503409Conflicto en la Direct API: ya existe un pago para este merchant_order_id, el Idempotency-Key se utilizó con un cuerpo distinto o para un pago Hosted, o la solicitud original sigue en curso.Consulte el pago con GET. Reenvíe únicamente una solicitud idéntica.

Validación

CódigoHTTPSignificadoAcción recomendada
1000001422La solicitud no es válida: un campo desconocido, un formato no válido, una cadena de consulta en un POST, un parámetro de consulta no admitido o un cuerpo que no es un objeto JSON. errors enumera los campos afectados.Corrija los campos indicados.
1000020422El formato del importe no es válido.Envíe una cadena que represente un importe positivo con dos decimales y sin ceros a la izquierda, por ejemplo "100.00".
1000021422La moneda no está admitida.Utilice USD, EUR, AED o TRY, u omita currency.
1000023422El importe es demasiado elevado.Envíe un importe menor.
9000008422El valor de webhook_url no es válido.Utilice una URL HTTPS pública en el puerto 443 con una longitud máxima de 500 caracteres.
9000038422El valor de success_url o fail_url no es válido.Utilice una URL HTTPS con una longitud máxima de 2048 caracteres. En sandbox también se admite http://localhost.

Importe, tarjeta y BIN

CódigoHTTPSignificadoAcción recomendada
9000005422El importe está fuera de los límites de su cuenta (Hosted API). limits contiene el rango permitido.Ajuste el importe.
0055235422El importe no se acepta (Direct API y comprobación de BIN). amount_limit contiene min y max cuando están disponibles.Ajuste el importe.
0055210422El BIN no tiene seis dígitos.Envíe los seis primeros dígitos del número de tarjeta.
0055233409El BIN es desconocido.Solicite al pagador otra tarjeta. No se trata de un rechazo del banco.
0055230409Esta tarjeta no puede utilizarse para este pago.Solicite al pagador otra tarjeta. No se trata de un rechazo del banco.
0055231409Esta tarjeta no se acepta para este pago.Solicite al pagador otra tarjeta.
0055221409Se ha alcanzado un límite de esta tarjeta.Utilice otra tarjeta o reintente más tarde.
0055234409Los datos de la tarjeta no son válidos: número de tarjeta, fecha de caducidad o CVV. metadata.developer_detail indica el campo: card_number_invalid, card_expiry_invalid o card_cvv_invalid.Solicite al pagador que vuelva a introducir los datos de la tarjeta.
0055001409Su cuenta no está lista para este pago. Con una clave sandbox, esto también significa que la simulación de sandbox no está disponible temporalmente (developer_detail: sandbox_unavailable), equivalente a 9000013.Póngase en contacto con soporte.
0055002403Los pagos con tarjeta no están habilitados para su cuenta.Póngase en contacto con soporte.
9000013409La operación de sandbox no está disponible; por ejemplo, cuando se llama a un endpoint exclusivo de sandbox con una clave de producción.Utilice una clave de sandbox o reintente más tarde.

No encontrado

CódigoHTTPSignificadoAcción recomendada
9000009404El pago no existe en esta cuenta y este entorno, o no puede utilizarse para esta operación.Verifique el payment_id y utilice una clave del mismo entorno.
1000002404No encontrado: la ruta es desconocida o este producto de API no está habilitado en la plataforma. Una API deshabilitada para su cuenta devuelve 403 en su lugar (9000040 en la Hosted API, 0055502 en la Direct API).Verifique la ruta; contacte con soporte si la API debería estar disponible.
1000007405El método HTTP no está permitido. Allow enumera los métodos válidos.Utilice el método documentado.

Límites y disponibilidad

CódigoHTTPSignificadoAcción recomendada
1000005429Demasiadas solicitudes, o bloqueo temporal tras fallos de firma repetidos.Espere el intervalo indicado en Retry-After y, a continuación, reintente con esperas crecientes.
1000011503El servicio no está disponible temporalmente.Reintente la misma solicitud. Consulte los recursos existentes antes de crear otros nuevos.
1000013503Esta función aún no está disponible.Reintente transcurridos retry_after segundos.
1000014503Hay un mantenimiento programado en curso. until y note proporcionan los detalles.Reintente una vez transcurrido el intervalo indicado en Retry-After.
3000001503El tipo de cambio no está disponible temporalmente.Reintente más tarde.
1000006500Se produjo un error inesperado en el servidor.Reintente la misma solicitud o consulte el recurso.

Operaciones de cuenta

CódigoHTTPSignificadoAcción recomendada
9000014409Este pago tiene un reembolso o una anulación abiertos o completados, o una disputa que no se ganó.Consulte el reembolso o la disputa.
9000015404La disputa no existe.Verifique el ID de la disputa.
9000016409La disputa ya no admite respuestas (está cerrada o ya se superó evidence_due_at).Consulte la disputa.
9000017422La dirección TRC20 no es válida.Verifique la dirección con su titular.
1000030422Esta dirección no puede recibir retiros.Utilice otra dirección.
9000018422address_acknowledged no es true.Confirme la dirección y, a continuación, envíe true.
9000019422El importe del retiro no es válido o es inferior al mínimo.Ajuste el importe.
9000020409Saldo disponible insuficiente.Compruebe el saldo disponible.
9000021409Se ha alcanzado el límite diario de retiros.Reintente al día siguiente o solicite un importe menor.
9000022409Ya se está procesando otro retiro.Espere y, a continuación, reenvíe la misma solicitud.
9000023404El retiro no existe.Verifique el ID del retiro.
9000024404La entrega de webhook no existe.Verifique el ID de la entrega.
9000025409Solo se pueden reenviar las entregas fallidas.Compruebe el estado de la entrega.

Solo en sandbox

CódigoHTTPSignificadoAcción recomendada
9000026503El sandbox aún no está disponible para esta operación.Reintente una vez transcurrido el intervalo indicado en Retry-After.
9000027409Otra operación está utilizando el saldo de prueba.Reenvíe la misma solicitud tras una breve espera.
9000030409No se pudo acreditar el saldo de prueba.Reintente más tarde.
9000033409El saldo de prueba es insuficiente para este reembolso. Un retiro en sandbox con saldo insuficiente devuelve 9000020 en su lugar.Cree primero pagos de prueba adicionales.

Idioma de los mensajes

title y detail se ajustan a Accept-Language (en, tr, de, ar, es, el; de-AT se trata como de). Con cualquier otro valor se devuelven en inglés. Content-Language indica el idioma utilizado.

Lista de comprobación para el paso a producción

Complete esta lista de comprobación antes de aceptar pagos reales.

  • En sandbox, un pago ha finalizado como succeeded y otro como failed, y los estados de sus pedidos se han actualizado en consecuencia.

  • Su endpoint de webhooks verifica la firma sobre el cuerpo sin procesar, rechaza las marcas de tiempo obsoletas, almacena cada event_id una sola vez y responde con 2xx sin demora.

  • Solo entrega los pedidos después de que un webhook verificado o un GET muestre succeeded, nunca basándose en el retorno del navegador.

  • Cada POST almacena su Idempotency-Key antes del envío y lo reutiliza tras errores de red.

  • Su código gestiona requires_action, processing, 429 y 5xx sin crear pagos duplicados.

  • La clave de producción y su clave secreta se almacenan únicamente en su gestor de secretos. Sustituir la clave de sandbox por la clave de producción es el único cambio en el código.

  • webhook_url, success_url y fail_url apuntan a sus direcciones HTTPS de producción.

  • Registra el X-Correlation-Id de cada solicitud.

Aunque las claves estén listas, verifique la integración con esta lista antes de aceptar pagos reales. Completar una prueba no es un requisito para generar claves.