Recoge la tarjeta en tu propio formulario de pago y envíala a Manuell desde tu servidor. El cliente confirma el pago con 3-D Secure y recibes el resultado por webhook.
Pagos reales. Se requiere una clave de producción.
Visión general
Tu servidor habla directamente con Manuell: pregunta qué flujo necesita una tarjeta, crea el pedido con los datos de la tarjeta y lo sigue hasta que se paga o falla.
URL base
https://www.manuellsolutions.com/api/v4
Moneda
Solo TRY. Un pago por pedido, sin cuotas.
Importes
Cadenas con 2 decimales, p. ej. "1500.00".
Entorno
sandbox o live, en la cabecera X-CEK-Environment. Debe coincidir con el entorno de tu clave.
Errores
application/problem+json (RFC 7807) con un code de 7 dígitos.
Límite de tasa
300 solicitudes por minuto y clave (600 por minuto y dirección IP antes de la autenticación). Al superarlo: 429 con Retry-After.
Duplicados
No existe Idempotency-Key. merchant_order_id protege frente a pedidos duplicados.
Consulta el BIN
Envía los 6 primeros dígitos de la tarjeta y el importe a GET /api/v4/bins/{bin}. payment_mode indica si debes recoger la tarjeta tú mismo (direct) o enviar al cliente a una página alojada (hosted).
Crea el pedido
Envía POST /api/v4/orders con la tarjeta (direct) o solo con el BIN (hosted). El pedido vuelve como requires_action, requires_hosted, pending o failed.
Termina y confirma
Lleva al cliente a redirect_url (3-D Secure) o a hosted_url. El resultado final llega por webhook; si se retrasa, consulta el pedido con GET /api/v4/orders/{merchantOrderId}.
Configuración en el panel
Genera una clave API y su secreto para cada entorno en “API Key e Integración”. Guarda el secreto en tu servidor.
Añade las IP públicas de tus servidores a la lista de IP permitidas del entorno. Mientras la lista esté vacía, cada llamada se rechaza con 403 y el código 0055501.
Define una dirección de webhook HTTPS. Su host debe ser público.
Define las direcciones de retorno de éxito y de fallo (HTTPS en producción). Sin ellas, el cliente se queda en la pantalla de resultado tras pagar.
Tu código de comercio es MRC- seguido de tu número de cuenta de 6 dígitos, por ejemplo MRC-123456.
Autenticación
Cada llamada lleva cabeceras X-CEK-*. La firma es un HMAC-SHA256 de una cadena que une la marca de tiempo, el método, la ruta y el hash del cuerpo.
Cabeceras obligatorias
Cabecera
Descripción
X-CEK-Api-Key
La clave API del entorno.
X-CEK-Environment
sandbox o live. Debe coincidir con el entorno de la clave.
X-CEK-Merchant-Code
Tu código de comercio, por ejemplo MRC-123456. Obligatorio en sandbox; en producción, si lo envías, debe coincidir con tu cuenta.
X-CEK-Timestamp
Hora Unix actual en segundos (10 dígitos). Una diferencia de más de 300 segundos se rechaza.
X-CEK-Signature
64 caracteres hex en minúsculas, calculados como se describe abajo.
Content-Type
application/json en las solicitudes POST.
Cómo firmar una solicitud
Toma el cuerpo exactamente como lo envías y calcula sha256(body) en hex minúsculas. Para una solicitud sin cuerpo usa el hash de la cadena vacía.
Une la marca de tiempo, el método en mayúsculas, la ruta y el hash del cuerpo con saltos de línea (\n). La ruta es la ruta completa sin host ni cadena de consulta, por ejemplo /api/v4/orders.
Calcula el HMAC-SHA256 de esa cadena con tu secreto de API como clave y escríbelo como 64 caracteres hex en minúsculas.
Envíala como X-CEK-Signature junto con las demás cabeceras.
Cada ejemplo consulta un BIN y crea un pedido en sandbox. Las credenciales salen de variables de entorno; nunca pongas el secreto en el código. Cada ejecución usa un merchant_order_id nuevo.
#!/usr/bin/env bash
# export MERCHANT_API_KEY=... MERCHANT_API_SECRET=... MERCHANT_CODE=MRC-123456 (sandbox key)
set -euo pipefail
BASE_URL="https://www.manuellsolutions.com"
signed() { # signed METHOD PATH [BODY]
local method="$1" path="$2" body="${3:-}" timestamp sha string_to_sign signature
timestamp="$(date +%s)"
sha="$(printf '%s' "$body" | openssl dgst -sha256 -hex | awk '{print $NF}')"
string_to_sign="$(printf '%s\n%s\n%s\n%s' "$timestamp" "$method" "${path%%\?*}" "$sha")"
# the secret travels in the environment, not on the command line
signature="$(printf '%s' "$string_to_sign" | python3 -c 'import hashlib,hmac,os,sys; print(hmac.new(os.environ["MERCHANT_API_SECRET"].encode(), sys.stdin.buffer.read(), hashlib.sha256).hexdigest())')"
local args=(--silent --show-error --request "$method" "$BASE_URL$path"
--header "X-CEK-Api-Key: $MERCHANT_API_KEY"
--header "X-CEK-Environment: sandbox"
--header "X-CEK-Merchant-Code: $MERCHANT_CODE"
--header "X-CEK-Timestamp: $timestamp"
--header "X-CEK-Signature: $signature")
if [ -n "$body" ]; then args+=(--header "Content-Type: application/json" --data-binary "$body"); fi
curl "${args[@]}"
echo
}
# 1) BIN lookup: no body, so the hash is sha256("")
signed GET "/api/v4/bins/435509?amount=1500.00"
# 2) create an order; a new merchant_order_id for every order
ORDER_ID="ORDER-$(date +%s)-$(openssl rand -hex 4)"
signed POST "/api/v4/orders" "{\"merchant_order_id\":\"$ORDER_ID\",\"amount\":\"1500.00\",\"card\":{\"holder_name\":\"TEST CUSTOMER\",\"number\":\"4355090000044242\",\"expiry_month\":\"12\",\"expiry_year\":\"30\",\"cvv\":\"123\"}}"
<?php
$config = [
'base' => 'https://www.manuellsolutions.com',
'key' => getenv('MERCHANT_API_KEY'),
'secret' => getenv('MERCHANT_API_SECRET'),
'code' => getenv('MERCHANT_CODE'),
'env' => 'sandbox',
];
function signedRequest(array $config, string $method, string $path, ?array $payload = null): array
{
$method = strtoupper($method);
$signingPath = explode('?', $path, 2)[0];
// serialise once: the same bytes are hashed and sent
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$timestamp = (string) time();
$stringToSign = implode("\n", [$timestamp, $method, $signingPath, hash('sha256', $body)]);
$signature = hash_hmac('sha256', $stringToSign, $config['secret']);
$headers = [
'X-CEK-Api-Key: ' . $config['key'],
'X-CEK-Environment: ' . $config['env'],
'X-CEK-Merchant-Code: ' . $config['code'],
'X-CEK-Timestamp: ' . $timestamp,
'X-CEK-Signature: ' . $signature,
];
if ($body !== '') {
$headers[] = 'Content-Type: application/json';
}
$ch = curl_init($config['base'] . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 35, // an order can take up to 30 seconds
]);
if ($body !== '') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($response === false) {
throw new RuntimeException('No response; repeat the same request or read the order with GET.');
}
return [$status, json_decode((string) $response, true)];
}
// 1) BIN lookup: no body, so the hash is sha256("")
[$status, $bin] = signedRequest($config, 'GET', '/api/v4/bins/435509?amount=1500.00');
echo $status, ' ', $bin['data']['payment_mode'] ?? '', PHP_EOL;
// 2) create an order; a new merchant_order_id for every order
[$status, $order] = signedRequest($config, 'POST', '/api/v4/orders', [
'merchant_order_id' => 'ORDER-' . time() . '-' . bin2hex(random_bytes(4)),
'amount' => '1500.00',
'card' => ['holder_name' => 'TEST CUSTOMER', 'number' => '4355090000044242', 'expiry_month' => '12', 'expiry_year' => '30', 'cvv' => '123'],
]);
echo $status, ' ', $order['data']['status'] ?? '', PHP_EOL;
const crypto = require("node:crypto");
const BASE_URL = "https://www.manuellsolutions.com";
const { MERCHANT_API_KEY, MERCHANT_API_SECRET, MERCHANT_CODE } = process.env;
async function signedRequest(method, path, payload) {
method = method.toUpperCase();
const signingPath = path.split("?", 1)[0];
// serialise once: the same bytes are hashed and sent
const body = payload === undefined ? "" : JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1000));
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const stringToSign = [timestamp, method, signingPath, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", MERCHANT_API_SECRET).update(stringToSign).digest("hex");
const headers = {
"X-CEK-Api-Key": MERCHANT_API_KEY,
"X-CEK-Environment": "sandbox",
"X-CEK-Merchant-Code": MERCHANT_CODE,
"X-CEK-Timestamp": timestamp,
"X-CEK-Signature": signature,
};
if (body !== "") headers["Content-Type"] = "application/json";
// an order can take up to 30 seconds
const res = await fetch(BASE_URL + path, { method, headers, body: body === "" ? undefined : body, signal: AbortSignal.timeout(35000) });
return { status: res.status, body: JSON.parse(await res.text()) };
}
(async () => {
// 1) BIN lookup: no body, so the hash is sha256("")
const bin = await signedRequest("GET", "/api/v4/bins/435509?amount=1500.00");
console.log(bin.status, bin.body.data?.payment_mode);
// 2) create an order; a new merchant_order_id for every order
const order = await signedRequest("POST", "/api/v4/orders", {
merchant_order_id: `ORDER-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`,
amount: "1500.00",
card: { holder_name: "TEST CUSTOMER", number: "4355090000044242", expiry_month: "12", expiry_year: "30", cvv: "123" },
});
console.log(order.status, order.body.data?.status);
})();
import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.request
import uuid
BASE_URL = "https://www.manuellsolutions.com"
API_KEY = os.environ["MERCHANT_API_KEY"]
API_SECRET = os.environ["MERCHANT_API_SECRET"]
MERCHANT_CODE = os.environ["MERCHANT_CODE"]
def signed_request(method, path, payload=None):
method = method.upper()
signing_path = path.split("?", 1)[0]
# serialise once: the same bytes are hashed and sent
body = json.dumps(payload, separators=(",", ":")) if payload is not None else ""
timestamp = str(int(time.time()))
string_to_sign = "\n".join([timestamp, method, signing_path, hashlib.sha256(body.encode()).hexdigest()])
signature = hmac.new(API_SECRET.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest()
headers = {
"X-CEK-Api-Key": API_KEY,
"X-CEK-Environment": "sandbox",
"X-CEK-Merchant-Code": MERCHANT_CODE,
"X-CEK-Timestamp": timestamp,
"X-CEK-Signature": signature,
}
if body:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(BASE_URL + path, data=body.encode() or None, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=35) as response: # an order can take up to 30 seconds
return response.status, json.loads(response.read())
except urllib.error.HTTPError as error: # problem+json body
return error.code, json.loads(error.read())
# 1) BIN lookup: no body, so the hash is sha256("")
status, bin_info = signed_request("GET", "/api/v4/bins/435509?amount=1500.00")
print(status, bin_info.get("data", {}).get("payment_mode"))
# 2) create an order; a new merchant_order_id for every order
status, order = signed_request("POST", "/api/v4/orders", {
"merchant_order_id": f"ORDER-{int(time.time())}-{uuid.uuid4().hex[:8]}",
"amount": "1500.00",
"card": {"holder_name": "TEST CUSTOMER", "number": "4355090000044242", "expiry_month": "12", "expiry_year": "30", "cvv": "123"},
})
print(status, order.get("data", {}).get("status"))
Consultar un BIN
GET/api/v4/bins/{bin}
Consulta el BIN de una tarjeta
Llámalo en cuanto el cliente haya escrito los 6 primeros dígitos. La respuesta es una instantánea que te ayuda a elegir entre tu propio formulario de tarjeta y la página alojada; la decisión final se toma al crear el pedido.
Parámetros
Campo
Tipo
Obligatorio
Descripción
bin
string · path
Obligatorio
Los seis primeros dígitos del número de tarjeta.
amount
string · query
Obligatorio
Importe del pedido en TRY con 2 decimales. La elegibilidad se evalúa para este importe.
Respuesta · 200
Campo
Tipo
Descripción
bin
string
El BIN consultado.
bank_name
string | null
Banco emisor, o null si se desconoce.
network
string | null
Red de la tarjeta: visa, mastercard, amex, troy o null.
card_type
string | null
credit, debit, prepaid o null.
card_program
string | null
Nombre del programa de la tarjeta, o null.
country
string | null
País emisor (ISO alfa-2), o null.
domestic
boolean
true si la tarjeta se emitió en Turquía.
payment_mode
string
direct: recoge la tarjeta tú mismo. hosted: usa la página alojada.
Un solo endpoint crea el pedido en ambos modos. Lo decide el cuerpo: envía card para un pedido direct o solo bin para uno hosted. Enviar ambos devuelve 422.
Pedido directo (con tarjeta)
Campo
Tipo
Obligatorio
Descripción
merchant_order_id
string
Obligatorio
Tu referencia del pedido: de 1 a 64 caracteres de A-Z a-z 0-9 . _ -, única por cuenta y entorno.
amount
string
Obligatorio
Importe en TRY con 2 decimales, por ejemplo "1500.00".
card.holder_name
string
Obligatorio
Nombre en la tarjeta, de 2 a 120 caracteres.
card.number
string
Obligatorio
Número de tarjeta, de 13 a 19 dígitos, solo cifras.
merchant_order_id debe ser único por cuenta y entorno. Es la única protección frente a duplicados.
Repetir una solicitud con el mismo merchant_order_id y el mismo contenido no inicia un pago nuevo y devuelve el pedido tal como está. Un contenido distinto devuelve 409 con el código 0055503.
Un campo que no está en el esquema devuelve 422 con el código 1000001.
Con una respuesta 4xx no se cobró nada. Usa un merchant_order_id nuevo en el siguiente intento. Un pedido failed también es definitivo.
Tras una respuesta 5xx o un tiempo de espera agotado, el pago puede haber empezado ya. No crees un pedido nuevo: repite la misma solicitud sin cambios o consulta el pedido con GET. Deja al menos 30 segundos antes de dar el tiempo por agotado.
Si en ese momento el BIN es hosted, una solicitud con card también devuelve requires_hosted y no se usan los datos de la tarjeta.
El objeto del pedido
Las llamadas de creación y lectura devuelven el pedido dentro de data.
Campo
Tipo
Descripción
order_code
string
Número de pedido, 9 dígitos como cadena. Es el valor que llega en la dirección de retorno.
merchant_order_id
string
Tu merchant_order_id.
status
string
requires_action, requires_hosted, pending, paid o failed.
amount
string
Importe del pedido, 2 decimales.
currency
string
Siempre TRY.
redirect_url
string | null
Dirección de 3-D Secure. Presente mientras el estado sea requires_action.
hosted_url
string | null
Dirección de la página alojada. Presente mientras el estado sea requires_hosted.
hosted_expires_at
string | null
Momento en que hosted_url deja de funcionar (ISO 8601, UTC).
failure_reason_code
string | null
Código de motivo de un pedido failed; consulta Motivos de rechazo. Puede ser null si nunca se intentó el pago.
failure_message
string | null
Texto corto para el cliente sobre el motivo. Siempre está en turco; ramifica según el código.
card
object | null
Resumen de la tarjeta (holder_name, bin, last_four, network, program, issuer_bank_name). Disponible de inmediato en direct; en hosted, cuando se conoce el resultado.
created_at
string
Hora de creación (ISO 8601, +00:00).
completed_at
string | null
Hora de finalización, o null mientras el pedido siga abierto.
El banco aún no ha respondido o el cliente abandonó la pantalla. Espera el webhook.
paid
Sí
El pago se cobró.
failed
Sí
El pago no se realizó. failure_reason_code explica el motivo.
El cliente dispone de 10 minutos para 3-D Secure. La página alojada funciona hasta hosted_expires_at. Un pago al que el banco nunca responde pasa a paid o failed en 24 horas, y un pago caducado pasa a failed.
En raras ocasiones, un pedido failed puede pasar a paid más tarde. Considera paid como la verdad.
Consultar un pedido
GET/api/v4/orders/{merchantOrderId}
Consulta un pedido
Devuelve el mismo objeto que la creación, sin cuerpo de solicitud. redirect_url se devuelve hasta que termina la ventana de 3-D Secure y hosted_url hasta hosted_expires_at.
Parámetros
Campo
Tipo
Obligatorio
Descripción
merchantOrderId
string · path
Obligatorio
El merchant_order_id que usaste al crear el pedido.
El webhook es el canal principal. Si se retrasa, consulta el pedido como mucho cada 30 segundos.
Tras crear el pedido, el cliente termina el pago en una página que no es tuya. Envíalo allí con una redirección normal de página completa.
3-D Secure (`requires_action`)
Redirige el navegador del cliente a redirect_url.
El cliente confirma el pago con su banco.
Cuando el pago es definitivo, el cliente va a tu dirección de retorno y recibes el webhook.
Página alojada (`requires_hosted`)
Lleva al cliente a hosted_url. Una redirección de página completa siempre funciona; mostrarla en un marco modal depende del permiso de la página, así que mantén la redirección como alternativa.
El cliente introduce la tarjeta y completa 3-D Secure en esa página. Un intento fallido no cierra el pedido; el cliente puede volver a intentarlo allí.
Si el cliente cierra la página, la misma hosted_url la vuelve a abrir hasta hosted_expires_at.
Cuando termina el pago, toda la página se envía a tu dirección de retorno. Confirma el resultado con el webhook.
Dirección de retorno
Cuando un pedido pasa a paid o failed, el cliente va a la dirección de retorno correspondiente que definiste en el panel. Si hay una dirección de webhook, la dirección recibe estos parámetros firmados.
Campo
Tipo
Descripción
order_code
string
El número de pedido de 9 dígitos. La dirección no incluye merchant_order_id; asocia el pedido con este valor.
status
string
paid o failed.
failure_code
string
Código de motivo, solo en un pedido failed que lo tenga. En otro caso el parámetro no aparece.
ts
integer
Hora Unix en segundos en que se construyó la dirección.
key_version
integer
Versión de tu secreto. Aumenta cada vez que lo renuevas.
sig
string
Firma, 64 caracteres hex en minúsculas.
Cadena de firma canónica
sig = hex(HMAC-SHA256(key = API_SECRET, message = ts + "." + order_code + "." + status + "." + failure_code))
Verifica sig antes de fiarte de los parámetros. Si falta failure_code, usa una cadena vacía en su lugar; el punto se mantiene.
Rechaza un ts de más de 10 minutos.
El retorno solo indica dónde está el cliente. El resultado es definitivo cuando llega el webhook o lo confirma GET /api/v4/orders/{merchantOrderId}.
Si no hay dirección de webhook, la dirección se abre sin estos parámetros.
Los resultados se entregan como solicitudes POST JSON firmadas a la dirección de webhook HTTPS del entorno.
Evento
Cuándo se envía
checkout.order.succeeded
El pedido está paid.
checkout.order.failed
El pedido está failed.
Cabecera
Cabecera
Descripción
X-CEK-Timestamp
Hora Unix en segundos. Rechaza una diferencia de más de 300 segundos.
X-CEK-Signature
Firma: hex del HMAC-SHA256 de la marca de tiempo, un punto y el cuerpo sin procesar, con tu secreto de API como clave.
X-CEK-Event-Id
UUID del evento. Igual que event_id en el cuerpo y no cambia en los reintentos.
X-CEK-Environment
sandbox o live.
Verifica cada notificación
Lee el cuerpo sin procesar antes de analizar el JSON.
Calcula la firma sobre X-CEK-Timestamp, un punto y el cuerpo sin procesar, y compárala con X-CEK-Signature en tiempo constante.
Rechaza la solicitud si la marca de tiempo difiere de tu reloj en más de 300 segundos.
Deduplica con X-CEK-Event-Id. La entrega es al menos una vez y puede llegar desordenada. Si has visto checkout.order.succeeded de un pedido, ignora cualquier checkout.order.failed del mismo.
Responde con cualquier 2xx en 10 segundos (3 para conectar). Las redirecciones (3xx), 4xx, 5xx y los tiempos de espera cuentan como fallo y se reintentan.
Ejemplos de verificación de firma
<?php
// Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>".
function isValidDirectWebhook(string $rawBody, string $timestamp, string $signature, string $secret): bool
{
if (preg_match('/^[0-9]{10}$/D', $timestamp) !== 1 || preg_match('/^[0-9a-f]{64}$/D', $signature) !== 1
|| abs(time() - (int) $timestamp) > 300) {
return false;
}
return hash_equals(hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret), $signature);
}
$rawBody = file_get_contents('php://input');
if (!isValidDirectWebhook($rawBody, $_SERVER['HTTP_X_CEK_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_CEK_SIGNATURE'] ?? '', getenv('MERCHANT_API_SECRET'))) {
http_response_code(401);
exit;
}
// Deduplicate with X-CEK-Event-Id, save the event, then answer 2xx.
http_response_code(204);
const crypto = require("node:crypto");
// Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>".
// rawBody must be the untouched request bytes (Buffer), not re-serialised JSON.
function isValidDirectWebhook(rawBody, timestamp, signature, secret) {
if (!/^[0-9]{10}$/.test(timestamp) || !/^[0-9a-f]{64}$/.test(signature)
|| Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(timestamp + "." + rawBody.toString("utf8")).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Then deduplicate with X-CEK-Event-Id, save the event and answer 2xx.
module.exports = { isValidDirectWebhook };
import hashlib
import hmac
import re
import time
def is_valid_direct_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
"""Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>"."""
if (not re.fullmatch(r"[0-9]{10}", timestamp) or not re.fullmatch(r"[0-9a-f]{64}", signature)
or abs(int(time.time()) - int(timestamp)) > 300):
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# Then deduplicate with X-CEK-Event-Id, save the event and answer 2xx.
Reintentos
Una entrega fallida se repite tras las esperas de abajo, contadas desde el fallo anterior, hasta 11 intentos en total.
Intento
Espera previa
1
—
2
60 s (1 min)
3
300 s (5 min)
4
900 s (15 min)
5
1800 s (30 min)
6
3600 s (1 h)
7
10800 s (3 h)
8
21600 s (6 h)
9
43200 s (12 h)
10
86400 s (24 h)
11
172800 s (48 h)
Cuerpo
El cuerpo incluye schema_version (3), event_id, event_type, un objeto order, un objeto merchant y el resumen de la tarjeta en payment_details.card. Un evento failed añade failure_code, failure_reason_code y failure_message. Ignora los campos que no uses; pueden aparecer campos nuevos.
failure_reason_code es uno de estos códigos. Basa tu lógica en el código; failure_message es un texto en turco para el cliente.
Código
Descripción
payment_declined
El banco emisor no aprobó el pago.
do_not_honor
El banco rechazó sin dar un motivo concreto.
insufficient_funds
El saldo o el límite de la tarjeta no alcanza para este pago.
card_limit_reached
La tarjeta alcanzó su límite de operaciones.
card_details_invalid
No se aceptó el número de tarjeta.
card_expiry_invalid
La fecha de caducidad no es válida.
card_security_code_invalid
El código de seguridad (CVV) es incorrecto.
card_not_active
La tarjeta no está habilitada para pagos.
card_brand_not_supported
Esta marca de tarjeta no es compatible con este pago.
three_ds_failed
No se pudo completar 3-D Secure.
security_declined
Un control de seguridad detuvo el pago. Indica al cliente solo que el banco no lo aprobó.
payment_not_permitted
Esta tarjeta no se puede usar para este pago.
amount_not_allowed
El importe está fuera del rango que acepta esta tarjeta.
currency_not_supported
La moneda no es compatible con este pago.
payment_details_rejected
Se rechazaron los datos del pago.
duplicate_payment
Parece que ya se cobró este mismo pago.
cancelled_by_customer
El cliente canceló antes de que terminara el pago.
payment_timed_out
Se agotó el tiempo del pago.
bank_unavailable
No se puede contactar con el banco ahora. Inténtalo de nuevo en breve.
bank_requested_retry
El banco pidió repetir el pago.
provider_unavailable
No se pudo completar el pago ahora. Inténtalo de nuevo en breve.
La lista puede crecer. Trata un código que no conozcas como payment_declined. En el webhook, security_declined se informa como payment_declined en failure_code.
Códigos de error
Los errores usan el formato application/problem+json. Ramifica según code; title y detail son para personas.
Campo
Tipo
Descripción
type, title, status, detail
string · integer
Campos estándar del formato problem.
instance
string
La ruta de la solicitud.
code
string
El código de error de 7 dígitos de la tabla de abajo.
correlation_id
string
Indícalo al contactar con soporte. También se envía como X-Correlation-Id.
metadata
object
developer_detail, retry_class y audit_event: información adicional para depurar.
amount_limit
object
Solo con 0055235: min y max del importe permitido, o null en un límite de volumen.
errors
object
Solo con 1000001: los campos que no superaron la validación.
application/problem+json
{
"type": "https://www.manuellsolutions.com/problems/0055233",
"title": "BIN not recognised",
"status": 409,
"detail": "No card range matches this BIN.",
"instance": "/api/v4/bins/999999",
"code": "0055233",
"correlation_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
"metadata": {
"developer_detail": "bin_not_found",
"retry_class": "no_retry",
"audit_event": "bin_lookup_rejected"
}
}
Las respuestas de error llevan además X-Correlation-Id, X-RateLimit-Limit, X-RateLimit-Remaining y Content-Language; un 429 añade Retry-After.
Las comprobaciones se hacen en este orden: esquema (1000001), importe (0055235), BIN (0055233, 0055230), tarjeta (0055234).
Código
HTTP
Significado
0055501
403
La lista de IP permitidas está vacía o la solicitud llega desde una dirección que no figura en ella.
0055502
401 / 403
Clave, entorno o código de comercio incorrectos, cuenta inactiva o acceso a la Direct API no abierto.
0055504
401
La marca de tiempo no tiene 10 dígitos o difiere en más de 300 segundos, o la firma no son 64 caracteres hex en minúsculas.
0055505
401
No se pudo verificar la firma.
1000001
422
El cuerpo no cumple el esquema: campo que falta, incorrecto o desconocido, o card junto con bin. errors enumera los campos.
1000005
429
Se superó el límite de tasa. Espera lo indicado en Retry-After.
0055001
409
La cuenta aún no está lista para cobrar; falta un paso de configuración.
0055002
403
Los pagos con tarjeta están desactivados para la cuenta.
0055235
422
El importe no es válido o está fuera de los límites (amount_limit presente), o se alcanzó un límite de volumen diario, semanal o mensual (amount_limit es null).
0055230
409
No hay un canal de pago disponible para este BIN.
0055233
409
No se pudo identificar el BIN.
0055210
422
bin no tiene exactamente seis dígitos.
0055234
409
El número de tarjeta o la fecha de caducidad no es válido.
0055221
409
Se alcanzó el límite diario de intentos o de importe de esta tarjeta.
0055231
409
Esta tarjeta no se puede cobrar en este momento.
0055503
409
Se usó el mismo merchant_order_id con contenido distinto, o la primera solicitud aún se está procesando.
0055402
404
No existe ningún pedido con este merchant_order_id.
0055404
409
Solo el endpoint de resultado de sandbox: el pedido no está ni pending ni requires_hosted.
Sandbox
Sandbox usa los mismos endpoints con una clave de sandbox y X-CEK-Environment: sandbox. No se mueve dinero real.
X-CEK-Merchant-Code es obligatorio en sandbox. Se usan las direcciones de webhook y de retorno del entorno sandbox.
Usa cualquier fecha de caducidad futura y cualquier CVV de 3 dígitos.
Los límites de volumen cuentan solo los pedidos de sandbox.
BIN
HTTP
Código de resultado
435509
200
direct
540061
200
hosted
979210
409
0055230
cualquier otro BIN
409
0055233
Número de tarjeta
Comportamiento
4355090000044242
requires_action. Aprueba la pantalla de prueba de 3-D Secure para llegar a paid.
4355090000005151
requires_action. Tras 3-D Secure el pedido pasa a failed con insufficient_funds.
4355090000089999
requires_action. Tras 3-D Secure el pedido sigue pending; ciérralo con el endpoint de resultado de sandbox.
4355090000040000
failed de inmediato con payment_declined, antes de 3-D Secure.
cualquier otra tarjeta válida
requires_action. En la pantalla de prueba de 3-D Secure eliges el resultado: éxito da paid, fallo da failed (payment_declined), tiempo agotado deja pending.
Un número de tarjeta no válido (Luhn) o una fecha de caducidad pasada devuelve 409 con el código 0055234. Un pedido hosted usa el BIN 540061; en su página de prueba eliges éxito (paid) o fallo (failed con insufficient_funds). Mientras siga en requires_hosted, también puedes cerrarlo con el endpoint de resultado de sandbox.
Cierra un pedido de sandbox que está en pending o, en un pedido hosted, aún espera su página hosted (requires_hosted). El webhook y la dirección de retorno funcionan como en producción.
Campo
Tipo
Obligatorio
Descripción
result
string
Obligatorio
paid o failed (un pedido fallido recibe el motivo payment_declined).