Ir al contenido
Documentación

Direct API

Con la Direct API, el formulario de tarjeta forma parte de su sitio web y su servidor envía los datos de la tarjeta a Payniko. El navegador del pagador solo se redirige a Payniko para 3-D Secure y para los BIN hosted. Utilice la Direct API cuando el formulario de tarjeta deba estar en su propia página.

Pasos de integración

  1. Compruebe el BIN

    Cuando el pagador haya introducido seis dígitos, llame a GET /api/payments/bin/{bin6}. payment_mode indica si debe recopilar los datos completos de la tarjeta en su formulario o enviar al pagador a la página de pago.

  2. Cree el pago

    Llame a POST /api/payments/direct con los datos de la tarjeta, o solo con bin6 para un BIN hosted.

  3. Complete la autenticación 3-D Secure

    Si el estado es requires_action, redirija el navegador a next_action.url. A continuación, el navegador regresa a su success_url o fail_url con parámetros firmados.

  4. Confirme el resultado

    Espere el webhook payment.succeeded o payment.failed, o llame a GET /api/payments/{payment_id}. Entregue el pedido solo con el estado succeeded.

Flujo de pago

Your server                          Payniko                        Payer's browser
 1  GET /api/payments/bin/{bin6} ───►  payment_mode: direct | hosted
 2  POST /api/payments/direct  ─────►  200  requires_action + next_action.url
                                            (or processing / succeeded / failed)
 3  redirect to next_action.url ──────────────────────────────────►  3-D Secure page
                                       ───────────────────────────►  success_url / fail_url
 4  webhook  ◄───────────────────────  payment.succeeded / payment.failed
    GET /api/payments/{payment_id} ──►  current status

Requisitos previos

  • La Direct API se habilita por cuenta. Sin este acceso, las solicitudes se rechazan con 403 0055502.
  • Las claves, los encabezados y la firma son idénticos a los de la Hosted API; consulte Autenticación. Los ejemplos utilizan las funciones auxiliares de solicitud.
GET/api/payments/bin/{bin6}

1. Comprobar el BIN

Antes de crear el pago, la comprobación de BIN indica si la tarjeta puede pagarse en su formulario (direct) o si el pago debe completarse en la página de pago de Payniko (hosted). La comprobación no genera ningún cargo.

Parámetros

CampoTipoObligatorioDescripciónEjemplo
bin6string · pathObligatorioSeis primeros dígitos de la tarjeta.540061
amountstring · queryObligatorioImporte con dos decimales, por ejemplo 100.00. Se comprueba con respecto a los límites de su cuenta.100.00
currencystring · queryOpcionalUSD, EUR, AED o TRY. De forma predeterminada, la moneda principal del sistema. Utilice la misma moneda al crear el pago.USD

Se rechaza cualquier otro parámetro de consulta. La cadena de consulta no forma parte de la firma.

./payniko.sh GET '/api/payments/bin/540061?amount=100.00&currency=USD'
import {api} from './payniko.mjs';

const lookup = await api('GET', '/api/payments/bin/540061?amount=100.00&currency=USD');
console.log(lookup);
<?php
require __DIR__ . '/payniko.php';

$lookup = payniko_api('GET', '/api/payments/bin/540061?amount=100.00&currency=USD');
print_r($lookup);
from payniko import api

lookup = api("GET", "/api/payments/bin/540061?amount=100.00&currency=USD")
print(lookup)

Respuesta · Ejemplo

{
  "data": {
    "bin6": "540061",
    "bank": "Test Bank",
    "network": "mastercard",
    "card_type": "credit",
    "card_program": null,
    "country": "TR",
    "domestic": true,
    "payment_mode": "hosted"
  }
}

Respuesta · 200

Devuelve { "data": ... } con los siguientes campos:

CampoTipoDescripción
bin6stringLos seis dígitos enviados.
bankstring | nullBanco emisor, si se conoce.
networkstring | nullRed de tarjetas, si se conoce.
visamastercardamextroy
card_typestring | nullTipo de tarjeta, si se conoce.
creditdebitprepaid
card_programstring | nullPrograma de la tarjeta. Actualmente siempre null.
countrystring | nullPaís emisor (ISO 3166-1 alfa-2), si se conoce.
domesticbooleantrue para una tarjeta nacional; false en caso contrario o si se desconoce.
payment_modestring | nulldirect: recopile los datos completos de la tarjeta. hosted: envíe al pagador a la página de pago. Cualquier otro valor, incluido null: no cree ningún pago.
directhosted

Tratamiento de la respuesta

  • direct: recopile los datos completos de la tarjeta y cree el pago con card.
  • hosted: cree el pago solo con bin6. El pagador introduce los datos de la tarjeta en la página de pago de Payniko.
  • Cualquier otro payment_mode o un error de tarjeta 409 (0055230, 0055231, 0055233): no cree ningún pago y solicite al pagador otra tarjeta con un mensaje neutro, ya que no se trata de un rechazo del banco. 422 0055235 indica un problema con el importe. Ante 429 o 5xx, repita la misma comprobación más tarde.

Ejemplo

import {api} from './payniko.mjs';

let mode = null;
try {
  const lookup = await api('GET', '/api/payments/bin/411111?amount=100.00&currency=USD');
  mode = lookup.data.payment_mode;
} catch (error) {
  // No answer, 429 or 5xx: temporary. Try the same lookup again later.
  if (error.status === undefined || error.status === 429 || error.status >= 500) throw error;
  // Other 4xx: error.problem.code says why, for example 0055233.
}
if (mode !== 'direct' && mode !== 'hosted') {
  // Also null: do not create a payment. Ask for another card with a neutral message.
}

Posibles errores

POST/api/payments/directRequiere Idempotency-Key

2. Crear el pago

Siempre devuelve 200 con el pago actual, también cuando se reenvía la misma solicitud. El rechazo de una tarjeta no es un error HTTP; la respuesta es 200 con status: failed.

Cuerpo de la solicitud

CampoTipoObligatorioDescripciónEjemplo
merchant_order_idstringObligatorioSu referencia de pedido: de 1 a 64 caracteres de A-Z a-z 0-9 . _ -. Un pago por referencia y entorno; la unicidad no distingue entre mayúsculas y minúsculas.ORDER-2026-002
amountstringObligatorioImporte con dos decimales, por ejemplo "100.00", dentro de los límites de su cuenta."100.00"
currencystringOpcionalDe forma predeterminada, la moneda principal del sistema. Utilice la misma moneda que en la comprobación de BIN.
USDEURAEDTRY
USD
webhook_urlstring · uriObligatorioURL HTTPS pública en el puerto 443 que recibe los webhooks de este pago, 500 caracteres como máximo. No se aceptan direcciones 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 cuando el pago se ha 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 en todos los demás casos. 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
cardobjectCondicionalLos datos de la tarjeta introducidos por el pagador (consulte la tabla siguiente). Envíe card o bin6, pero no ambos.{ … }
bin6stringCondicionalSolo para un BIN hosted: los seis primeros dígitos. El pagador introduce los datos de la tarjeta en la página de pago. Una solicitud para un BIN direct que solo incluye bin6 devuelve 409 0055230.540061
card.numberstringObligatorio13–19 dígitos sin espacios, con un dígito de control válido (15 dígitos para American Express).4355090000044242
card.holder_namestringObligatorioNombre que figura en la tarjeta, de 2 a 120 caracteres.JONAS KAZLAUSKAS
card.expiry_monthstringObligatorioDos dígitos, de 01 a 12.12
card.expiry_yearstringObligatorioDos dígitos, por ejemplo 30. La tarjeta no debe estar caducada y el año puede estar como máximo 30 años en el futuro.30
card.cvvstringObligatorioExactamente 3 dígitos, o 4 dígitos para American Express. Nunca lo almacene ni lo registre.123

Todos los valores de la tarjeta son cadenas. Los campos desconocidos dentro de card se rechazan.

Con un BIN hosted, 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. La Direct API devuelve los mensajes de errors traducidos; la tabla muestra el texto en inglés.

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.

Con datos de tarjeta · Ejemplo

{
  "merchant_order_id": "ORDER-2026-002",
  "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"
  },
  "card": {
    "number": "4355090000044242",
    "holder_name": "JONAS KAZLAUSKAS",
    "expiry_month": "12",
    "expiry_year": "30",
    "cvv": "123"
  }
}
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/payments/direct' '{
  "merchant_order_id": "ORDER-2026-002",
  "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"
  },
  "card": {
    "number": "4355090000044242",
    "holder_name": "JONAS KAZLAUSKAS",
    "expiry_month": "12",
    "expiry_year": "30",
    "cvv": "123"
  }
}' "$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/direct', {
  "merchant_order_id": "ORDER-2026-002",
  "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"
  },
  "card": {
    "number": "4355090000044242",
    "holder_name": "JONAS KAZLAUSKAS",
    "expiry_month": "12",
    "expiry_year": "30",
    "cvv": "123"
  }
}, 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/direct', [
    "merchant_order_id" => 'ORDER-2026-002',
    "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',
    ],
    "card" => [
        "number" => '4355090000044242',
        "holder_name" => 'JONAS KAZLAUSKAS',
        "expiry_month" => '12',
        "expiry_year" => '30',
        "cvv" => '123',
    ],
], $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/direct", {
    "merchant_order_id": "ORDER-2026-002",
    "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"
    },
    "card": {
        "number": "4355090000044242",
        "holder_name": "JONAS KAZLAUSKAS",
        "expiry_month": "12",
        "expiry_year": "30",
        "cvv": "123"
    }
}, key)
print(payment)

Respuesta · Ejemplo

{
  "data": {
    "payment_id": "3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35",
    "merchant_order_id": "ORDER-2026-002",
    "flow": "direct",
    "status": "requires_action",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": {
      "type": "redirect",
      "url": "https://payniko.com/example-authentication-step",
      "expires_at": "2026-11-01T12:30:00.000Z"
    },
    "card": {
      "bin6": "435509",
      "last_four": "4242",
      "network": "visa",
      "bank": "Test Bank"
    },
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-01T12:30:00.000Z",
    "completed_at": null
  }
}

Con bin6 para un BIN hosted · Ejemplo

{
  "merchant_order_id": "ORDER-2026-003",
  "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"
  },
  "bin6": "540061"
}

Interpretación de la respuesta

EstadoAcción recomendada
requires_actionRedirija el navegador a next_action.url (3-D Secure o, para un BIN hosted, la página de pago).
processingEspere el webhook y no cree un pago nuevo.
succeededEntregue el pedido.
failedMuestre failure.message y solicite al pagador otra tarjeta.
  • La llamada puede tardar varios segundos mientras Payniko se comunica con el banco. Use un tiempo de espera de al menos 60 segundos y, tras agotarse, reenvíe la solicitud con la misma Idempotency-Key y el mismo cuerpo. Mientras la primera solicitud siga en curso (hasta 3 minutos), el reintento devuelve 409 0055503 con metadata.retry_class: retry_same_request: espere unos segundos y reenvíe la misma solicitud.
  • Para un BIN hosted, se ignora cualquier card enviado y el pagador continúa en la página de pago.
  • Si ya existe un pago para el merchant_order_id, o se envía el mismo Idempotency-Key con un cuerpo distinto, la solicitud se rechaza con 409 0055503.
  • Los datos de tarjeta no válidos se rechazan con 409 0055234. metadata.developer_detail identifica el campo: card_number_invalid, card_expiry_invalid o card_cvv_invalid. Solicite al pagador que vuelva a introducir los datos de la tarjeta.
  • Los datos de contacto ausentes o no válidos se rechazan con 422 1000001; errors indica customer, customer.email o customer.phone.

3. 3-D Secure y el navegador

Cuando el estado es requires_action, el navegador del pagador debe abrir next_action.url, donde el banco realiza la autenticación 3-D Secure o el pagador continúa en la página de pago.

  • Redirija la página completa a next_action.url o ábrala en una pestaña nueva; ambos métodos se admiten siempre.
  • La página no vuelve a solicitar los datos de la tarjeta; solo recopila los datos del navegador que requiere el banco.
  • Utilice siempre la next_action.url más reciente antes de su next_action.expires_at (el final del paso de 3-D Secure). No vuelva a abrir nunca una URL anterior.
  • A continuación, el navegador se redirige a success_url (solo con succeeded) o a fail_url con parámetros firmados (consulte URL de retorno). Después, consulte el pago.

Carga en marcos

La página de 3-D Secure no puede mostrarse dentro de un marco; redirija la página completa o abra una pestaña nueva. Solo la página de pago de un BIN hosted puede cargarse en un marco, únicamente con claves de producción y solo en el origen exacto de la success_url del pago. La página de pago de la Hosted API no puede cargarse nunca en un marco.

La página de pago de un BIN hosted integra la propia pantalla segura de pago y de 3-D Secure del proveedor de pagos y puede mostrarse como iframe dentro de su propio formulario de pago.

Si carga en un marco la página de pago de un BIN hosted, los mensajes que esta envía a su ventana sirven únicamente como señal para comprobar el pago. Acéptelos solo desde el origen de next_action.url y desde su marco, ignore su contenido y haga que su servidor consulte el pago.

Navegador

// Only if you show the payment page of a hosted BIN in a frame on the exact origin of success_url.
// nextActionUrl: next_action.url from your server.
const pageOrigin = new URL(nextActionUrl).origin;
window.addEventListener('message', (event) => {
  if (event.origin !== pageOrigin || event.source !== frame.contentWindow) return;
  // A wake-up call only: ignore event.data. Ask your server to read the payment.
  fetch('/checkout/ORDER-2026-002/refresh', {method: 'POST', credentials: 'same-origin'});
});

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": "3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35",
    "merchant_order_id": "ORDER-2026-002",
    "flow": "direct",
    "status": "requires_action",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": {
      "type": "redirect",
      "url": "https://payniko.com/example-authentication-step",
      "expires_at": "2026-11-01T12:30:00.000Z"
    },
    "card": {
      "bin6": "435509",
      "last_four": "4242",
      "network": "visa",
      "bank": "Test Bank"
    },
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-01T12:30:00.000Z",
    "completed_at": null
  }
}

Estados de los pagos Direct

EstadoSignificadoAcción recomendada
requires_actionEl navegador debe abrir next_action.url para 3-D Secure o para la página de pago de un BIN hosted.Redirija al pagador a next_action.url.
processingLa respuesta del banco está pendiente. Este estado también puede devolverse directamente en la respuesta de creación.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 antes de expires_at.Si el pagador aún desea pagar, cree un pago nuevo.
cancelledEl pago se canceló.No entregue el pedido.
EscenarioEstadoEvento
Aprobado tras 3-D Securerequires_action → succeededpayment.succeeded
Rechazadofailedpayment.failed
Respuesta del banco pendienteprocessingNinguno hasta el resultado final
Sin respuesta del banco; se agotó el tiempo de espera del intentoprocessing → failed / expiredpayment.failed
El pagador no inició el pago y venció el plazo (si el pagador abandona el pago a mitad del proceso, este puede finalizar como failed)expiredpayment.failed

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.

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.

Direct API: si el resultado aún no es definitivo cuando el pagador vuelve de 3-D Secure, el navegador no se redirige a fail_url; el pagador permanece en una página de resultado neutral de Payniko. Confíe en el webhook o en un GET para conocer el resultado.

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/3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35'
import {api} from './payniko.mjs';

const payment = await api('GET', '/api/payments/3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35');
console.log(payment);
<?php
require __DIR__ . '/payniko.php';

$payment = payniko_api('GET', '/api/payments/3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35');
print_r($payment);
from payniko import api

payment = api("GET", "/api/payments/3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35")
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 _ -.
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.
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.
0055210422El BIN no tiene seis dígitos.Envíe los seis primeros dígitos del número de tarjeta.
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.
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.
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.
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.
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.
1000011503El servicio no está disponible temporalmente.Reintente la misma solicitud. Consulte los recursos existentes antes de crear otros nuevos.

Los errores de la Direct API incluyen además metadata.retry_class (no_retry, retry_same_request, retry_after, read_order). Una ventana del navegador cerrada o un tiempo de espera agotado en su lado no significa que el pago haya fallado; consulte el pago. Para ver la lista completa, consulte Códigos de error.

Pruebas con tarjetas de sandbox

Con una clave de sandbox, la comprobación de BIN y el pago utilizan datos de prueba fijos: 435509 y 400000 son BIN direct, y 540061 es un BIN hosted. Utilice 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.

Con la Direct API, una tarjeta de prueba rechazada devuelve failed directamente en la respuesta de creación; un requires_action intermedio solo se produce en la página de pago de un BIN hosted. Base sus decisiones en el webhook o en un GET.

Ejemplo completo

Un servidor Node.js mínimo que abarca la comprobación de BIN, la creación del pago, la redirección a 3-D Secure, la URL de 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: Direct API from start to end (Node.js 20+, no dependencies).
import http from 'node:http';
import {randomUUID} from 'node:crypto';
import {api} from './payniko.mjs';                  // request helper (Authentication)
import {verifyWebhook} from './verify-webhook.mjs'; // webhook check (Webhooks)
import {verifyReturn} from './verify-return.mjs';   // return URL check (Return URL)

const site = 'https://merchant.example';
const orders = new Map();      // use your database
const seenEvents = new Set();  // use your database
const readBody = async (req) => { const parts = []; for await (const p of req) parts.push(p); return Buffer.concat(parts); };

const reply = (res, data) => {
  res.writeHead(200, {'Content-Type': 'application/json'});
  res.end(JSON.stringify(data.status === 'requires_action' && data.next_action
    ? {redirect: data.next_action.url} : {status: data.status, failure: data.failure}));
};

http.createServer(async (req, res) => {
  const url = new URL(req.url, site);
  // 1. BIN check, 2. create the payment. Your own HTTPS form posts the card here.
  if (req.method === 'POST' && url.pathname === '/pay') {
    const form = JSON.parse((await readBody(req)).toString('utf8'));
    const bin6 = form.number.slice(0, 6);
    let mode = null;
    try {
      mode = (await api('GET', `/api/payments/bin/${bin6}?amount=100.00&currency=USD`)).data.payment_mode;
    } catch (error) {
      if (error.status === undefined || error.status >= 500 || error.status === 429) throw error;
    }
    if (mode !== 'direct' && mode !== 'hosted') { res.writeHead(422).end('Please use another card.'); return; }
    const orderId = 'ORDER-' + Date.now();
    const key = randomUUID();
    orders.set(orderId, {key, status: 'new'});
    const body = {
      merchant_order_id: orderId, amount: '100.00', currency: 'USD',
      customer: {email: form.email, phone: form.phone},
      webhook_url: `${site}/webhooks/payniko`,
      success_url: `${site}/payment/success`, fail_url: `${site}/payment/failure`,
    };
    if (mode === 'direct') {
      body.card = {number: form.number, holder_name: form.holder_name,
        expiry_month: form.expiry_month, expiry_year: form.expiry_year, cvv: form.cvv};
    } else {
      body.bin6 = bin6;                               // payer enters the card on the payment page
    }
    const {data} = await api('POST', '/api/payments/direct', body, key);
    Object.assign(orders.get(orderId), {paymentId: data.payment_id, status: data.status});
    reply(res, data);                                 // 3. browser goes to next_action.url
    return;
  }
  // Return URL: show a page, but trust only the webhook or a GET.
  if (req.method === 'GET' && (url.pathname === '/payment/success' || url.pathname === '/payment/failure')) {
    const signed = verifyReturn(url.searchParams, () => process.env.API_SECRET);
    let status = 'unknown';
    if (signed) status = (await api('GET', `/api/payments/${url.searchParams.get('payment_id')}`)).data.status;
    res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
    res.end(status === 'succeeded' ? 'Thank you, payment received.' : 'Payment not completed.');
    return;
  }
  // Webhook: verify, store event_id once, answer 2xx fast.
  if (req.method === 'POST' && url.pathname === '/webhooks/payniko') {
    const raw = await readBody(req);
    if (!verifyWebhook(raw, req.headers, process.env.API_SECRET)) { res.writeHead(401).end(); return; }
    const event = JSON.parse(raw.toString('utf8'));
    if (!seenEvents.has(event.event_id)) {          // use a unique index in your database
      seenEvents.add(event.event_id);
      const order = orders.get(event.data.merchant_order_id);
      if (order && event.event_type.startsWith('payment.')) order.status = event.data.status;
    }
    res.writeHead(204).end();
    return;
  }
  res.writeHead(404).end();
}).listen(8080);