API de pago Konbit
Cobra un pago desde la billetera Konbit de tus clientes. Intégralo en pocas líneas, sandbox gratis.
Cómo funciona
Tu servidor crea una intención de pago. El cliente la paga en su app Konbit con su PIN. Recibes un webhook firmado y entregas tu servicio digital.
1. Crea una intención
Una llamada de servidor con el monto. Recibes un enlace de pago y un id.
2. El cliente paga
Abre el enlace, confirma con su PIN. Sin cargos para él: paga el monto mostrado.
3. Te notifican
Un webhook firmado confirma el pago. Entregas el servicio. El dinero llega a tu saldo Konbit.
amountMinor es un entero en la unidad más pequeña: 10000 = 100,00 DOP. Nunca decimales.
Inicio en 5 minutos
Tres pasos: tu servidor crea una intención, el cliente la paga en Konbit, tu servidor confirma el pago antes de entregar. Aquí está la llamada completa y la respuesta real, anotada.
curl -X POST https://app.getkonbit.com/api/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-123" \
-H "Content-Type: application/json" \
-d '{"amountMinor":10000,"description":"Order 123","metadata":{"orderId":"order-123"},"returnUrl":"https://mon-site.com/merci"}'
{
"id": "5Qx0u5gexpnqsw6qQhks", // konbitRef — gardez-le
"status": "REQUIRES_PAYMENT",
"amountMinor": 10000, // 100,00 DOP
"currency": "DOP",
"feeMinor": 200, // notre commission
"code": "562183", // saisie manuelle / QR
"checkoutUrl": "https://app.getkonbit.com/pay/5Qx0u5gexpnqsw6qQhks",
"expiresAtMs": 1783710307539 // millisecondes
}
checkoutUrl: la página donde paga el cliente. code: entrada manual / QR. feeMinor: nuestra comisión, ya descontada de tu saldo (el cliente paga amountMinor). expiresAtMs: tras lo cual la intención pasa a EXPIRED.
La redirección a returnUrl ocurre en el navegador del cliente y puede falsificarse (basta con abrir la URL). NUNCA entregues tu servicio solo por el retorno. Confirma siempre en el servidor: webhook firmado o un GET del estado.
Obtener tu acceso
Dos niveles, presentados con honestidad.
Clave de prueba — inmediata
Genera una clave sk_test_ arriba: válida 7 días, sandbox, SIN webhooks. Ideal para explorar la API por polling. No se mueve dinero real.
Clave de comercio — en 24 h
Escríbenos con tu empresa y tu caso de uso. Creamos un comercio dedicado: clave persistente + webhook firmado. Solemos responder en 24 h hábiles. (El autoservicio de comercios llegará; hoy lo abrimos nosotros, por cumplimiento.)
Guarda el secreto en secreto
La clave sk_ se muestra UNA vez y vive solo en el servidor: nunca en un navegador, una app móvil, un repositorio Git o una URL. Guárdala en un gestor de secretos. ¿Comprometida? Pide una rotación: la anterior se revoca al instante.
Genera una clave de prueba
Pruébalo ya, sin cuenta. Una clave de prueba válida 7 días, modo sandbox.
Válida 7 días. Solo modo prueba.
No se pudo generar una clave ahora. Inténtalo de nuevo.
Pagar una intención de prueba (sandbox)
Sin cuenta Konbit: llama a POST /v1/test/pay con el id de la intención. Konbit simula un pagador sandbox, pasa la intención a SUCCEEDED, envía el webhook firmado (si diste una URL) y acredita tu saldo de comercio. Así pruebas todo el ciclo por tu cuenta.
curl -X POST https://app.getkonbit.com/api/v1/test/pay \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"intentId":"<id renvoyé par /payment_intents>"}'
# -> l'intention passe à SUCCEEDED, le webhook signé part (si configuré),
# et ton solde marchand augmente (testable par GET /balance + payouts).
La clave generada aquí es efímera (7 días), sandbox: sin dinero real. Basta para completar create → pay → webhook → payout por tu cuenta. Para una clave PERSISTENTE y producción real, contáctanos — un comercio dedicado + el cumplimiento llegan en el go-live. ¿Por qué «test» en la clave (sk_test_)? Es el prefijo sandbox: no se mueve dinero real. En sandbox, la «tarjeta de prueba» = POST /v1/test/pay (pagador sintético); toda página de pago muestra un banner «SANDBOX». Las claves de producción (sk_live_) las emite Konbit tras validación.
Intenciones de pago
REQUIRES_PAYMENT: pendiente · SUCCEEDED: pagado · EXPIRED: expirado · CANCELED: cancelado por ti · REFUNDED: reembolsado.
Montos
amountMinor es un entero en unidades menores (10000 = 100,00 DOP). Límites: 1 000 a 5 000 000, es decir 10 a 50 000 DOP. Solo DOP en V1 (aún no hay HTG en la API).
Idempotency-Key (obligatoria)
Requerida al crear. Verificado: la MISMA clave (por comercio) devuelve SIEMPRE la misma intención, sin duplicado ni doble cobro. Usa tu id de pedido — así un reintento de red no hace daño.
Conciliación por metadata
Adjunta tus identificadores (orderId, userId…) en metadata (máx. 20 claves). Vuelven tal cual en el webhook y el GET, para asociar un pago a tu pedido. NO pongas secretos ahí: el pagador y tu webhook los ven.
Referencia de la API
Autentica cada solicitud con tu clave secreta en el encabezado Authorization.
/v1/payment_intentsCrear una intención de pago
curl -X POST https://app.getkonbit.com/api/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-123" \
-H "Content-Type: application/json" \
-d '{"amountMinor":10000,"description":"Order 123","returnUrl":"https://mon-site.com/merci"}'
{
"id": "5Qx0u5gexpnqsw6qQhks",
"status": "REQUIRES_PAYMENT",
"amountMinor": 10000,
"currency": "DOP",
"feeMinor": 200,
"code": "562183",
"checkoutUrl": "https://app.getkonbit.com/pay/5Qx0u5gexpnqsw6qQhks",
"expiresAtMs": 1783710307539
}
Redirige a tu cliente a checkoutUrl (o muestra el QR) para que pague.
/v1/payment_intents/{id}Consultar el estado de un pago
{ "id": "5Qx0...", "status": "SUCCEEDED", "amountMinor": 10000, "feeMinor": 200 }
/v1/refundsReembolsar un pago
curl -X POST https://app.getkonbit.com/api/v1/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"payment_intent":"5Qx0u5gexpnqsw6qQhks"}'
/v1/balanceConsultar tu saldo
{ "object": "balance", "balanceMinor": 9800, "totalFeesMinor": 200, "currency": "DOP" }
/v1/payoutsPaga parte de tu saldo al wallet de un chofer / beneficiario, por su código Konbit (9 caracteres). Idempotency-Key obligatoria.
Se requiere clave de comercio (contrato Konbit): las claves de prueba del portal no pueden pagar a una billetera real — su saldo sandbox es ficticio.
curl -X POST https://app.getkonbit.com/api/v1/payouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: driver-42-2026-W27" \
-H "Content-Type: application/json" \
-d '{"konbitCode":"K7M2P9QRS","amountMinor":85000,"metadata":{"driverId":"drv_42"}}'
{ "id":"p_8Kd...", "object":"payout", "status":"SUCCEEDED", "amountMinor":85000, "feeMinor":0, "currency":"DOP", "konbitCode":"K7M2P9QRS" }
/v1/payoutsLista tus pagos, para la conciliación.
Estados de un pago
REQUIRES_PAYMENT: pendiente · SUCCEEDED: pagado · EXPIRED: expirado · CANCELED: cancelado por ti · REFUNDED: reembolsado.
Códigos de error
Todo error devuelve el mismo objeto: un código estable (para comprobar) y un mensaje legible.
{ "error": { "code": "amount_out_of_bounds", "message": "..." } }
| Código | Cuándo |
|---|---|
401 unauthorized | Clave ausente, inválida o revocada. |
400 idempotency_key_required | Falta la cabecera Idempotency-Key al crear. |
400 amount_out_of_bounds / invalid_amount | Monto fuera de los límites configurados, o no entero. |
400 invalid_return_url | returnUrl indicado pero no en https. |
404 not_found | Intención desconocida o de otro comercio. |
404 code_not_found | Código Konbit del beneficiario no encontrado (payout). |
409 not_cancelable / not_refundable | Estado incompatible (ya pagado, ya reembolsado, no cancelable…). |
409 insufficient_balance | Saldo del comercio insuficiente para el reembolso o el payout. |
429 quota_exceeded | Demasiadas solicitudes. |
503 service_unavailable | Konbit Pay desactivado temporalmente. |
Webhooks
Cada cambio de estado dispara un POST JSON firmado a tu webhookUrl (https obligatorio). Cabecera Konbit-Signature: t=
POST (votre webhookUrl)
Konbit-Signature: t=1720000000000,v1=<hmac_sha256>
{
"id": "evt_9f2c...", // id de l'ÉVÉNEMENT — dédup
"type": "payment_intent.succeeded", // .refunded · .canceled · .expired · .payment_failed (data.failureCode)
"createdAtMs": 1720000000000, // millisecondes
"data": {
"id": "5Qx0...", // id de l'intention (= konbitRef)
"status": "SUCCEEDED",
"amountMinor": 10000,
"currency": "DOP",
"feeMinor": 200,
"metadata": { "orderId": "order-123" }
}
}
El t de la firma Y createdAtMs están en milisegundos desde el epoch (13 dígitos). Trampa n.º 1: tratarlos como segundos hace fallar la ventana de 5 minutos y rechazas webhooks válidos.
Verificar la firma, paso a paso
1. Lee el cuerpo CRUDO (sin parsear). 2. Extrae t y v1 de la cabecera. 3. Rechaza si |ahora − t| > 5 min. 4. Calcula HMAC-SHA256(secreto_webhook, "
Verificación — fragmentos para copiar
const crypto = require("crypto");
// IMPORTANT: raw body, pas express.json() sur cette route
app.post("/konbit/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const raw = req.body; // Buffer brut
const sig = req.get("Konbit-Signature") || "";
const parts = Object.fromEntries(sig.split(",").map(p => p.split("=")));
const t = Number(parts.t); // MILLISECONDES
if (!t || Math.abs(Date.now() - t) > 5 * 60 * 1000) return res.sendStatus(401);
const expected = crypto.createHmac("sha256", process.env.KONBIT_WEBHOOK_SECRET)
.update(t + "." + raw.toString("utf8")).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(raw.toString("utf8"));
if (alreadyProcessed(event.id)) return res.sendStatus(200); // dédup sur event.id
// re-vérifiez event.data.amountMinor + currency contre VOTRE commande
markProcessed(event.id);
fulfill(event.data.metadata.orderId);
res.sendStatus(200);
});
<?php
$raw = file_get_contents("php://input"); // corps brut
$sig = $_SERVER["HTTP_KONBIT_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $sig), $p); // t=...&v1=...
$t = (int)($p["t"] ?? 0); // MILLISECONDES
if (!$t || abs(round(microtime(true)*1000) - $t) > 300000) { http_response_code(401); exit; }
$expected = hash_hmac("sha256", $t . "." . $raw, getenv("KONBIT_WEBHOOK_SECRET"));
if (!hash_equals($expected, $p["v1"] ?? "")) { http_response_code(401); exit; }
$event = json_decode($raw, true);
if (already_processed($event["id"])) { http_response_code(200); exit; } // dédup
// re-vérifiez amountMinor + currency contre votre commande
mark_processed($event["id"]);
fulfill($event["data"]["metadata"]["orderId"]);
http_response_code(200);
import hmac, hashlib, time, os
from flask import request, abort
@app.post("/konbit/webhook")
def konbit_webhook():
raw = request.get_data() # bytes bruts
parts = dict(p.split("=", 1) for p in request.headers.get("Konbit-Signature", "").split(","))
t = int(parts.get("t", 0)) # MILLISECONDES
if not t or abs(time.time() * 1000 - t) > 300000:
abort(401)
expected = hmac.new(os.environ["KONBIT_WEBHOOK_SECRET"].encode(),
f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(401)
event = request.get_json()
if already_processed(event["id"]): # dédup sur event.id
return "", 200
# re-vérifiez amountMinor + currency contre votre commande
mark_processed(event["id"])
fulfill(event["data"]["metadata"]["orderId"])
return "", 200
Política de reintentos
Entrega inmediata en el evento. Ante un fallo (respuesta no 2xx, o superado el tiempo de 10 s), Konbit reintenta 5 veces, con un backoff de 1, 5, 30, 120 y 720 minutos — 6 intentos en total durante unas 14,5 horas antes de rendirse (estado «dead», visible para diagnóstico). Cada intento se REFIRMA con un t actual: tu ventana de 5 min acepta los reintentos. El id del evento no cambia entre reintentos — deduplica por él.
O por consulta (polling)
Dos formas de saber que un pago tuvo éxito: recibir el webhook, o consultar la intención hasta el estado SUCCEEDED.
curl https://app.getkonbit.com/api/v1/payment_intents/{id} \
-H "Authorization: Bearer sk_test_..."
# répétez jusqu'à "status": "SUCCEEDED"
Webhook Y polling de conciliación, nunca uno sin el otro. El webhook te avisa rápido; un GET periódico de las intenciones recientes recupera cualquier entrega perdida (endpoint caído un momento, evento «dead»). Nunca dependas de un solo canal.
Pagar una intención de prueba
Para llegar al pago necesitas una cuenta Konbit con saldo. En sandbox: abre el checkoutUrl con sesión en una cuenta Konbit de prueba, o pídenos una cuenta pagadora de prueba con fondos. Sin eso puedes crear intenciones pero no liquidarlas. Una vez activado el cobro con tarjeta para tu cuenta (ver abajo), el mismo checkoutUrl también ofrece «Pagar con tarjeta» (sin cuenta Konbit): la intención queda marcada paidViaCard, se acredita a tu saldo USD y recibes card_payment.succeeded + payment_intent.succeeded (deduplica por id).
Checklist de puesta en producción
Once puntos antes de cambiar. Los tres en negrita son los que el 90 % de los integradores olvida — y que cuestan dinero o abren una brecha.
- Clave sk_ de comercio obtenida y guardada en el servidor (gestor de secretos), nunca en el cliente.
- Idempotency-Key puesta en cada creación (tu orderId).
- Montos en unidades menores, dentro de los límites; moneda DOP.
- Cuerpo CRUDO conservado para la verificación HMAC (sin parsear antes de la firma).
- Endpoint de webhook en https, responde 2xx en menos de 10 s, trabajo pesado en asíncrono.
- Firma verificada en tiempo constante, t tratado como milisegundos, solicitudes de más de 5 min rechazadas.
- Polling de conciliación como red de seguridad, además del webhook.
- Estados EXPIRED / CANCELED / REFUNDED y códigos de error gestionados con cuidado.
- Deduplicación por el id del EVENTO (entrega al-menos-una-vez).
- Reverificar el monto Y la moneda del webhook contra tu pedido — no confíes solo en el tipo de evento.
- Ningún camino del lado del cliente marca «pagado»: returnUrl no es prueba, solo lo es la confirmación del servidor.
Rail USD: cobra con tarjeta, paga por Zelle
Para plataformas que operan en dólares (inversión, nómina, marketplaces): tus clientes pagan con tarjeta en un checkout alojado por Konbit — sin cuenta Konbit — y tú distribuyes USD por Zelle, por lotes, con confirmación por webhook línea por línea.
1. Cobro con tarjeta
Tu servidor crea la intención (monto fijado en el servidor), el pagador ingresa su tarjeta en /cpay. La comisión de Konbit se suma al monto; tu saldo USD recibe el neto.
2. Fondos asegurados
Cada pago pasa por un período de retención antifraude antes de ser distribuible. Saldo en tres estados: pendiente, disponible, reservado.
3. Pagos por Zelle
Envías la lista (hasta 200 beneficiarios por lote). Konbit ejecuta cada transferencia y te notifica por webhook firmado: pagado o fallido, línea por línea.
El cobro con tarjeta (rail USD) es una activación POR COMERCIO, aprobada por Konbit (cumplimiento, cláusula de contracargo) — nunca en una clave de prueba. Una vez activado, tus enlaces de pago DOP también ofrecen la opción tarjeta al pagador, sin cuenta Konbit. Escríbenos para activarlo.