API de S1mpl3
Confirmá desde tu sistema si un SINPE Móvil llegó, por cuánto y si ya se usó. Todo responde JSON en español y se llama desde tu servidor.
02Autenticación#
Mandá la clave en cada solicitud, en Authorization: Bearer sk_live_… o en la cabecera X-API-Key. Las claves se muestran una sola vez; en el servidor solo guardamos su hash SHA-256. Si perdés una, revocala y generá otra.
Cada clave tiene un límite de 120 solicitudes por minuto. Al pasarlo recibís 429 con Retry-After: 30.
Authorization: Bearer sk_live_…
# o bien
X-API-Key: sk_live_…03Verificar un pago#
/verificarBusca el SINPE por los últimos 4 dígitos del comprobante y, si lo encuentra disponible, lo marca como usado en la misma operación. Un segundo intento con el mismo comprobante responde ya_utilizado.
| Campo | Tipo | Descripción |
|---|---|---|
ultimos4 obligatorio | string | Últimos 4 dígitos de la referencia. Mandalo como texto: "0427" conserva el cero inicial. |
monto recomendado | number | string | Monto esperado en colones (12500 o "12,500.00"). Evita que dos pagos se confundan. |
telefono | string | Teléfono de origen, para desempatar. |
referencia | string | Referencia completa del comprobante. |
referencia_externa | string | Tu número de orden; queda guardado junto al SINPE. |
consumir | boolean | Por defecto true. Con false solo consulta. |
# Verificar y marcar como usado
curl -X POST https://s1mpl3.net/api/v1/verificar \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"ultimos4":"0427","monto":12500,"referencia_externa":"ORD-1042"}'// Node 18+ (fetch incluido)
const r = await fetch("https://s1mpl3.net/api/v1/verificar", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.S1MPL3_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ ultimos4: "0427", monto: 12500, referencia_externa: "ORD-1042" }),
});
const d = await r.json();
if (d.verificado) liberarPedido("ORD-1042");$ch = curl_init("https://s1mpl3.net/api/v1/verificar");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("S1MPL3_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["ultimos4" => "0427", "monto" => 12500, "referencia_externa" => "ORD-1042"]),
]);
$d = json_decode(curl_exec($ch), true);import os, requests
r = requests.post(
"https://s1mpl3.net/api/v1/verificar",
headers={"Authorization": "Bearer " + os.environ["S1MPL3_KEY"]},
json={"ultimos4": "0427", "monto": 12500, "referencia_externa": "ORD-1042"},
timeout=15,
)
d = r.json(){
"verificado": true,
"consumido": true,
"transaccion": {
"id": 42,
"referencia": "2026101800000000000000427",
"ultimos4": "0427",
"monto": 12500,
"moneda": "CRC",
"telefono": "60000001",
"nombre": "ROCIO EJEMPLO MORA",
"entidad": "Banco de Costa Rica",
"motivo": "ORD-1042",
"banco": "BCR",
"fuente": "email",
"fecha": "2026-10-18T21:02:00.000Z",
"recibido_en": "2026-10-18T21:02:31.000Z",
"estado": "utilizado",
"utilizado_en": "2026-10-18T21:03:10.000Z",
"referencia_externa": "ORD-1042"
}
}04Qué puede salir mal#
Los errores responden "verificado": false, un código en error y un mensaje legible.
| HTTP | error | Qué significa |
|---|---|---|
| 404 | no_encontrado | No llegó ningún SINPE con esos dígitos en los últimos 30 días. |
| 409 | ya_utilizado | Ese comprobante ya se usó. Trae utilizado_en y referencia_externa. |
| 409 | ambiguo | Varios SINPE disponibles comparten esos 4 dígitos. Mandá monto, telefono o referencia. |
| 422 | monto_no_coincide | Existe, pero por otro monto. Trae monto_recibido y moneda. |
| 400 | parametros_invalidos | ultimos4 no tiene 4 dígitos o el monto no es válido. |
| 401 | no_autorizado | Falta la clave o está revocada. |
| 403 | cliente_inactivo | La cuenta está inactiva. |
| 429 | demasiadas_solicitudes | Pasaste el límite. Esperá y reintentá con espera exponencial. |
| 500 | error_interno | Algo falló de nuestro lado. |
{
"verificado": false,
"error": "ambiguo",
"mensaje": "Varios SINPE coinciden con esos 4 dígitos. Envíe monto, telefono o la referencia completa para desambiguar.",
"coincidencias": 2
}{
"verificado": false,
"error": "monto_no_coincide",
"mensaje": "Se encontró un SINPE con esos dígitos pero por un monto distinto (CRC 8 000,00).",
"monto_recibido": 8000,
"moneda": "CRC"
}05Consultar sin marcar#
/verificar/{ultimos4}Igual que verificar, pero nunca marca el comprobante como usado. Acepta ?monto=, ?telefono= y ?referencia= en la URL. Sirve para mostrarle al cliente que su pago llegó antes de cerrar la orden.
curl "https://s1mpl3.net/api/v1/verificar/0427?monto=12500" \
-H "Authorization: Bearer sk_live_…"{
"verificado": true,
"consumido": false,
"transaccion": {
"id": 42,
"referencia": "2026101800000000000000427",
"ultimos4": "0427",
"monto": 12500,
"moneda": "CRC",
"telefono": "60000001",
"nombre": "ROCIO EJEMPLO MORA",
"entidad": "Banco de Costa Rica",
"motivo": "ORD-1042",
"banco": "BCR",
"fuente": "email",
"fecha": "2026-10-18T21:02:00.000Z",
"recibido_en": "2026-10-18T21:02:31.000Z",
"estado": "disponible",
"utilizado_en": null,
"referencia_externa": null
}
}06Transacciones#
/transaccionesLos SINPE recibidos, del más reciente al más viejo.
| Parámetro | Descripción |
|---|---|
estado | disponible o utilizado. |
desde | Fecha ISO 8601; solo lo recibido desde ese momento. |
limit | Cuántas devolver: 50 por defecto, 200 como máximo. |
offset | Para paginar. |
/transacciones/{referencia}Una transacción por su referencia completa. Si no existe responde 404 con no_encontrado.
curl "https://s1mpl3.net/api/v1/transacciones?estado=disponible&limit=20" \
-H "Authorization: Bearer sk_live_…"{
"transacciones": [
{
"id": 42,
"referencia": "2026101800000000000000427",
"ultimos4": "0427",
"monto": 12500,
"moneda": "CRC",
"telefono": "60000001",
"nombre": "ROCIO EJEMPLO MORA",
"entidad": "Banco de Costa Rica",
"motivo": "ORD-1042",
"banco": "BCR",
"fuente": "email",
"fecha": "2026-10-18T21:02:00.000Z",
"recibido_en": "2026-10-18T21:02:31.000Z",
"estado": "disponible",
"utilizado_en": null,
"referencia_externa": null
}
]
}07Sincronizar#
/sincronizarFuerza una lectura inmediata de tus buzones activos. Normalmente no hace falta: los leemos cada 30 s. Devuelve, por buzón, cuántos correos se leyeron (fetched), cuántos SINPE nuevos se registraron (registered), cuántos se descartaron y cuántos ya existían.
curl -X POST https://s1mpl3.net/api/v1/sincronizar \
-H "Authorization: Bearer sk_live_…"{
"ok": true,
"buzones": [
{
"buzon": 1,
"fetched": 3,
"registered": 1,
"rejected": 2,
"duplicates": 0
}
]
}08SMS reenviados#
/ingest/smsPara bancos que avisan por SMS: una app reenviadora en el teléfono del comercio manda el texto del mensaje y lo registramos como SINPE.
| Campo | Descripción |
|---|---|
texto obligatorio | El SMS tal como llegó. |
banco | Pista del banco (por ejemplo BAC). |
remitente | Número o nombre que envió el SMS. |
Responde 201 si lo registró, 200 si esa referencia ya existía, y 422 con formato_no_reconocido o no_es_credito si el texto no sirve.
curl -X POST https://s1mpl3.net/api/v1/ingest/sms \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{"banco":"BAC","texto":"BAC: Ha recibido una transferencia SINPE Movil por CRC 12,500.00 de ROCIO EJEMPLO MORA. Ref: 2026101800000427. Detalle: ORD-1043"}'{
"creada": true,
"transaccion": {
"id": 42,
"referencia": "2026101800000427",
"ultimos4": "0427",
"monto": 12500,
"moneda": "CRC",
"telefono": null,
"nombre": "ROCIO EJEMPLO MORA",
"entidad": "BAC",
"motivo": "ORD-1043",
"banco": "BAC",
"fuente": "sms",
"fecha": null,
"recibido_en": "2026-10-18T21:02:31.000Z",
"estado": "disponible",
"utilizado_en": null,
"referencia_externa": null
}
}09Sandbox#
/sandbox/transaccionesSolo para cuentas con el modo sandbox habilitado (si no, responde 403 sandbox_desactivado). Simula un SINPE recibido para que pruebes la integración sin mover plata. En el panel, el Simulador hace lo mismo con escenarios listos (duplicado, monto distinto, no llegó, varios coinciden) y te muestra el payload del webhook. monto es obligatorio; referencia, nombre, telefono, motivo, entidad y moneda son opcionales.
curl -X POST https://s1mpl3.net/api/v1/sandbox/transacciones \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{"monto":12500,"motivo":"ORD-1043","referencia":"2026101800000000000000427"}'{
"creada": true,
"transaccion": {
"id": 42,
"referencia": "2026101800000000000000427",
"ultimos4": "0427",
"monto": 12500,
"moneda": "CRC",
"telefono": null,
"nombre": "CLIENTE DE PRUEBA",
"entidad": "SANDBOX",
"motivo": "ORD-1043",
"banco": "SANDBOX",
"fuente": "sandbox",
"fecha": "2026-10-18T21:02:00.000Z",
"recibido_en": "2026-10-18T21:02:31.000Z",
"estado": "disponible",
"utilizado_en": null,
"referencia_externa": null
}
}10Webhooks#
Registrá una URL en el panel y te mandamos un POST con JSON por cada evento al que la suscribas.
| Evento | Cuándo se envía |
|---|---|
sinpe.recibido | Entró un SINPE (crédito) a uno de tus buzones, por SMS o por el sandbox. Es el evento por defecto. |
sinpe.utilizado | Un comprobante se verificó con consumir: true y quedó marcado como usado (desde la caja o la API). |
ping | Prueba manual desde el panel (botón «Ping»). transaccion viene en null. |
| Cabecera | Contenido |
|---|---|
X-S1mpl3-Event | El nombre del evento. |
X-S1mpl3-Timestamp | Segundos Unix del envío. |
X-S1mpl3-Signature | sha256= + HMAC-SHA256 en hex de timestamp + "." + cuerpo, con el secreto del webhook. |
Respondé con un 2xx en menos de 10 s. Si no, reintentamos con espera creciente (30 s, 1 min, 2 min… hasta 1 h entre intentos), con un máximo de 8 intentos. En el panel ves cada entrega con su request, la respuesta de tu servidor y los intentos, y podés reenviarla o rotar el secreto.
/verificar con consumir en true: así el comprobante queda marcado como usado de forma atómica.{
"evento": "sinpe.recibido",
"id_entrega": 15,
"enviado_en": "2026-10-18T21:02:35.000Z",
"transaccion": {
"id": 42,
"referencia": "2026101800000000000000427",
"ultimos4": "0427",
"monto": 12500,
"moneda": "CRC",
"telefono": "60000001",
"nombre": "ROCIO EJEMPLO MORA",
"entidad": "Banco de Costa Rica",
"motivo": "ORD-1042",
"banco": "BCR",
"fuente": "email",
"fecha": "2026-10-18T21:02:00.000Z",
"recibido_en": "2026-10-18T21:02:31.000Z",
"estado": "disponible",
"utilizado_en": null,
"referencia_externa": null
}
}// Express: el cuerpo tiene que llegar crudo para calcular la firma
import crypto from "node:crypto";
app.post("/webhooks/s1mpl3", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-S1mpl3-Timestamp") || "";
const firma = req.get("X-S1mpl3-Signature") || "";
const esperada = "sha256=" + crypto
.createHmac("sha256", process.env.S1MPL3_WEBHOOK_SECRET)
.update(ts + "." + req.body.toString("utf8"))
.digest("hex");
const valida = firma.length === esperada.length &&
crypto.timingSafeEqual(Buffer.from(firma), Buffer.from(esperada));
if (!valida || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const evento = JSON.parse(req.body);
// evento.transaccion.ultimos4, evento.transaccion.monto…
res.sendStatus(200);
});$cuerpo = file_get_contents("php://input");
$ts = $_SERVER["HTTP_X_S1MPL3_TIMESTAMP"] ?? "";
$firma = $_SERVER["HTTP_X_S1MPL3_SIGNATURE"] ?? "";
$esperada = "sha256=" . hash_hmac("sha256", $ts . "." . $cuerpo, getenv("S1MPL3_WEBHOOK_SECRET"));
if (!hash_equals($esperada, $firma) || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
$evento = json_decode($cuerpo, true);
// $evento["transaccion"]["ultimos4"], $evento["transaccion"]["monto"]…
http_response_code(200);11Cómo leemos los pagos#
- Conectás el buzón al que tu banco manda los avisos, con una contraseña de aplicación. Se guarda cifrada con AES-256-GCM y solo leemos: nunca borramos ni enviamos correos.
- Revisamos el buzón cada 30 s. El correo tiene que venir de un dominio bancario conocido (o de un remitente que agregues) y pasar DKIM, ARC o SPF; si no, queda en «Correos descartados» con el motivo.
- Plantillas propias: Banco de Costa Rica, BAC Credomatic, Banco Nacional (leemos el PDF adjunto) y Banco Popular (también por SMS). Para Promerica, Davivienda, Scotiabank, Coopenae, Mutual Cartago, Grupo Mutual, Lafise, Banco Cathay, Banco BCT usamos una plantilla genérica por etiquetas.
- Guardamos monto, teléfono, nombre, entidad, motivo y referencia. Los avisos de dinero que sale (débitos) se guardan pero nunca se pueden verificar ni disparan webhooks.
- No pedimos credenciales bancarias y no podemos mover dinero.
12Buenas prácticas#
- Mandá siempre el monto: 4 dígitos solos pueden coincidir entre pagos.
- Pedile al cliente que escriba su número de orden en el motivo del SINPE y usalo como
referencia_externa. - Ante
ambiguo, pedí el teléfono o la referencia completa. - Ante
429, esperá y reintentá con espera exponencial. - Verificá la firma de cada webhook y descartá los que tengan un timestamp viejo.
- Guardá la clave en una variable de entorno, nunca en el código del navegador ni en el repositorio.