| id |
type |
title |
provider |
status |
integration_type |
created_at |
updated_at |
simco_version |
tags |
| INT-006 |
Integration |
Pagos CoDi/SPEI |
Banxico/STP |
Mock |
Pagos QR instantáneos |
2026-01-10 |
2026-01-17 |
4.0.1 |
| pagos |
| codi |
| spei |
| qr |
| banxico |
| transferencias |
|
INT-006: Pagos CoDi/SPEI
Metadata
| Campo |
Valor |
| Codigo |
INT-006 |
| Proveedor |
Banxico/STP |
| Tipo |
Pagos QR instantáneos |
| Estado |
Mock (en desarrollo) |
| Multi-tenant |
Si |
| Fecha planeada |
2026-Q1 |
| Owner |
Backend Team |
1. Descripcion
Integración con el sistema de Cobro Digital (CoDi) de Banxico y transferencias SPEI a través de STP (Sistema de Transferencias y Pagos). Permite a los changarros recibir pagos instantáneos mediante códigos QR, sin comisiones por transacción. CoDi opera sobre la infraestructura de SPEI, garantizando transferencias en tiempo real 24/7.
Casos de uso principales:
- Generación de códigos QR para cobro instantáneo
- Recepción de pagos SPEI sin comisión
- Notificación en tiempo real de pagos recibidos
- Conciliación automática de transacciones
- Generación de comprobantes electrónicos de pago
2. Credenciales Requeridas
Variables de Entorno
| Variable |
Descripcion |
Tipo |
Obligatorio |
| STP_EMPRESA |
Identificador de empresa en STP |
string |
SI |
| STP_CLAVE_PRIVADA |
Llave privada para firmar mensajes |
string |
SI |
| STP_CERTIFICADO |
Certificado digital .cer |
string |
SI |
| STP_CUENTA_CLABE |
CLABE de la cuenta receptora |
string |
SI |
| STP_WEBHOOK_URL |
URL para recibir notificaciones |
string |
SI |
| CODI_MERCHANT_ID |
ID de comercio registrado en CoDi |
string |
SI |
| CODI_API_KEY |
Llave de API de CoDi |
string |
SI |
Ejemplo de .env
# Pagos CoDi/SPEI
STP_EMPRESA=MICHANGARRITO
STP_CLAVE_PRIVADA=/path/to/private_key.pem
STP_CERTIFICADO=/path/to/certificate.cer
STP_CUENTA_CLABE=646180157000000001
STP_WEBHOOK_URL=https://api.michangarrito.mx/webhooks/stp
CODI_MERCHANT_ID=codi_mer_xxxxxxxx
CODI_API_KEY=codi_key_xxxxxxxx
3. Arquitectura
┌─────────────────────────────────────────────────────────────────┐
│ MiChangarrito │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Frontend │───▶│ API Server │───▶│ CoDiService │ │
│ │ (POS/App) │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
└────────────────────────────────────────────────────┼────────────┘
│
┌─────────────────────────┼─────────────┐
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ STP Gateway │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Banxico SPEI / CoDi │ │
│ │ (Red Interbancaria MX) │ │
│ └──────────────────────────────┘ │
│ Sistema Financiero │
└───────────────────────────────────────┘
Flujo de Pago CoDi
- Comercio genera solicitud de cobro con monto
- Sistema genera código QR con datos CoDi
- Cliente escanea QR con app bancaria
- Banco del cliente procesa transferencia SPEI
- STP notifica la recepción del pago
- Sistema actualiza estado de la venta en tiempo real
4. Endpoints
Endpoints Consumidos (STP/CoDi API)
| Método |
Endpoint |
Descripción |
| POST |
/v1/codi/cobros |
Generar solicitud de cobro CoDi |
| GET |
/v1/codi/cobros/{id} |
Consultar estado de cobro |
| POST |
/v1/spei/ordenPago |
Registrar orden de pago SPEI |
| GET |
/v1/spei/consultaOrden/{id} |
Consultar orden SPEI |
| GET |
/v1/cuentas/{clabe}/movimientos |
Consultar movimientos |
Endpoints Expuestos (MiChangarrito)
| Método |
Endpoint |
Descripción |
| POST |
/api/v1/payments/codi/generate |
Generar QR de cobro |
| GET |
/api/v1/payments/codi/{id}/status |
Consultar estado |
| GET |
/api/v1/payments/codi/{id}/qr |
Obtener imagen QR |
| POST |
/api/v1/webhooks/stp |
Recibir notificaciones STP |
| GET |
/api/v1/payments/spei/movements |
Listar movimientos |
5. Rate Limits
Limites de STP API
| Recurso |
Limite |
Periodo |
Estrategia |
| Generacion de cobros CoDi |
100 |
por minuto |
Throttling |
| Consulta de estado |
300 |
por minuto |
Cache 10s |
| Consulta de movimientos |
60 |
por minuto |
Cache 30s |
| Ordenes SPEI |
50 |
por minuto |
Queue |
Limites de Banxico/SPEI
| Tipo de Operacion |
Limite |
Observaciones |
| Monto maximo por transferencia |
$8,000 MXN |
Limite CoDi estandar |
| Monto maximo SPEI |
Sin limite |
Depende del banco |
| Transferencias por dia |
Sin limite |
Sujeto a monitoreo AML |
Estrategia de Retry
// src/integrations/codi/codi.retry.ts
export const codiRetryConfig = {
maxRetries: 3,
initialDelay: 1000, // 1 segundo
maxDelay: 30000, // 30 segundos
backoffMultiplier: 2,
retryableStatuses: [408, 429, 500, 502, 503, 504],
// Errores especificos de STP que permiten retry
retryableSTPCodes: [
'STP_TIMEOUT',
'STP_SERVICE_UNAVAILABLE',
'STP_RATE_LIMITED'
]
};
6. Manejo de Errores
Codigos de Error STP/CoDi
| Codigo |
Mensaje |
Causa |
Accion |
CODI_001 |
Comercio no registrado |
CLABE no asociada a CoDi |
Verificar registro con Banxico |
CODI_002 |
Monto excede limite |
Mayor a $8,000 MXN |
Usar SPEI directo |
CODI_003 |
QR expirado |
Timeout de 5 minutos |
Generar nuevo QR |
CODI_004 |
Cuenta invalida |
CLABE con digito verificador incorrecto |
Validar CLABE |
STP_001 |
Firma invalida |
Error en certificado o llave privada |
Verificar credenciales |
STP_002 |
Cuenta inexistente |
CLABE destino no existe |
Notificar al usuario |
STP_003 |
Fondos insuficientes |
Saldo insuficiente en cuenta origen |
N/A (lado cliente) |
STP_004 |
Servicio no disponible |
Mantenimiento STP |
Reintentar con backoff |
STP_005 |
Operacion duplicada |
Referencia ya procesada |
Verificar estado existente |
SPEI_001 |
Banco destino no disponible |
Banco fuera de linea |
Reintentar o notificar |
Ejemplo de Manejo de Errores
// src/integrations/codi/codi.error-handler.ts
import { Injectable, Logger } from '@nestjs/common';
import { CodiError, STPError } from './codi.types';
@Injectable()
export class CodiErrorHandler {
private readonly logger = new Logger(CodiErrorHandler.name);
async handleError(error: CodiError | STPError): Promise<PaymentErrorResponse> {
this.logger.error(`CoDi/STP Error: ${error.code}`, {
code: error.code,
message: error.message,
referencia: error.referencia,
timestamp: new Date().toISOString()
});
switch (error.code) {
case 'CODI_002':
return {
success: false,
error: 'AMOUNT_EXCEEDED',
message: 'El monto excede el limite de CoDi ($8,000 MXN). Use transferencia SPEI.',
suggestion: 'spei_transfer',
retryable: false
};
case 'CODI_003':
return {
success: false,
error: 'QR_EXPIRED',
message: 'El codigo QR ha expirado. Genere uno nuevo.',
suggestion: 'regenerate_qr',
retryable: true
};
case 'STP_004':
return {
success: false,
error: 'SERVICE_UNAVAILABLE',
message: 'El servicio de pagos no esta disponible temporalmente.',
suggestion: 'retry_later',
retryable: true,
retryAfter: 30 // segundos
};
default:
return {
success: false,
error: 'PAYMENT_ERROR',
message: 'Error procesando el pago. Intente nuevamente.',
retryable: true
};
}
}
}
7. Fallbacks
Estrategia de Fallback
┌─────────────────┐
│ Pago CoDi │
│ (Primario) │
└────────┬────────┘
│ Falla?
▼
┌─────────────────┐
│ QR Alternativo │
│ (Transferencia │
│ SPEI manual) │
└────────┬────────┘
│ Falla?
▼
┌─────────────────┐
│ Notificacion │
│ Manual + Datos │
│ de Cuenta │
└─────────────────┘
Configuracion de Fallbacks
// src/integrations/codi/codi.fallback.ts
export const codiFallbackConfig = {
// Fallback 1: QR con datos de transferencia SPEI
speiQR: {
enabled: true,
includeData: ['clabe', 'beneficiario', 'concepto', 'monto'],
qrFormat: 'EMVCo' // Compatible con apps bancarias
},
// Fallback 2: Datos para transferencia manual
manualTransfer: {
enabled: true,
showBankData: true,
copyToClipboard: true
},
// Notificaciones en modo degradado
notifications: {
sms: true, // Enviar datos por SMS
email: true // Enviar datos por email
}
};
Modo Degradado
Cuando STP/CoDi no esta disponible:
- Deteccion automatica: Health check cada 30 segundos
- Activacion: Despues de 3 fallos consecutivos
- Comportamiento:
- Mostrar QR con datos CLABE para transferencia manual
- Habilitar conciliacion manual en backoffice
- Alertar al administrador del tenant
- Recuperacion: Automatica cuando servicio responde OK
// src/integrations/codi/codi.health.ts
@Injectable()
export class CodiHealthService {
private degradedMode = false;
private consecutiveFailures = 0;
async checkHealth(): Promise<boolean> {
try {
const response = await this.stpClient.ping();
if (response.ok) {
this.consecutiveFailures = 0;
this.degradedMode = false;
return true;
}
} catch (error) {
this.consecutiveFailures++;
if (this.consecutiveFailures >= 3) {
this.degradedMode = true;
this.notifyAdmin('CoDi en modo degradado');
}
}
return false;
}
isDegraded(): boolean {
return this.degradedMode;
}
}
8. Multi-tenant
Modelo de Credenciales por Tenant
Cada tenant de MiChangarrito tiene su propia cuenta CLABE registrada en CoDi/STP, permitiendo:
- Recepcion directa de pagos a cuenta del comercio
- Conciliacion independiente por tenant
- Reportes fiscales separados
Estructura SQL
-- Tabla de configuracion CoDi por tenant
CREATE TABLE tenant_codi_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Credenciales STP
stp_empresa VARCHAR(50) NOT NULL,
stp_cuenta_clabe VARCHAR(18) NOT NULL,
stp_certificado_path VARCHAR(255),
stp_private_key_encrypted TEXT,
-- Credenciales CoDi
codi_merchant_id VARCHAR(50),
codi_api_key_encrypted TEXT,
-- Configuracion
webhook_secret_encrypted TEXT,
is_active BOOLEAN DEFAULT true,
sandbox_mode BOOLEAN DEFAULT false,
-- Limites personalizados
daily_limit DECIMAL(12,2) DEFAULT 50000.00,
transaction_limit DECIMAL(12,2) DEFAULT 8000.00,
-- Auditoria
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_tenant_codi UNIQUE(tenant_id),
CONSTRAINT valid_clabe CHECK (LENGTH(stp_cuenta_clabe) = 18)
);
-- Indice para busqueda por CLABE (notificaciones entrantes)
CREATE INDEX idx_tenant_codi_clabe ON tenant_codi_config(stp_cuenta_clabe);
Ejemplo de Contexto Multi-tenant
// src/integrations/codi/codi.context.ts
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class CodiContextService {
constructor(
@Inject(REQUEST) private request: Request,
private readonly configRepo: TenantCodiConfigRepository
) {}
async getCodiConfig(): Promise<TenantCodiConfig> {
const tenantId = this.request['tenantId'];
const config = await this.configRepo.findByTenantId(tenantId);
if (!config || !config.isActive) {
throw new PaymentConfigurationError(
'CoDi no configurado para este comercio'
);
}
return {
...config,
privateKey: await this.decryptKey(config.stpPrivateKeyEncrypted),
apiKey: await this.decryptKey(config.codiApiKeyEncrypted)
};
}
async generateCodiQR(amount: number, concept: string): Promise<CodiQR> {
const config = await this.getCodiConfig();
return this.codiService.generateQR({
clabe: config.stpCuentaClabe,
monto: amount,
concepto: concept,
merchantId: config.codiMerchantId,
referencia: this.generateReference()
});
}
}
9. Webhooks
Endpoints Registrados para Notificaciones STP
| Evento |
Endpoint |
Metodo |
| Pago recibido |
/api/v1/webhooks/stp/payment-received |
POST |
| Pago rechazado |
/api/v1/webhooks/stp/payment-rejected |
POST |
| Devolucion |
/api/v1/webhooks/stp/refund |
POST |
| Estado de orden |
/api/v1/webhooks/stp/order-status |
POST |
Validacion de Firma Digital
STP firma todas las notificaciones con su certificado. Es obligatorio validar la firma antes de procesar.
// src/integrations/codi/codi.webhook-validator.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import * as crypto from 'crypto';
@Injectable()
export class CodiWebhookValidator {
private readonly stpPublicKey: string;
constructor(private configService: ConfigService) {
this.stpPublicKey = this.configService.get('STP_PUBLIC_KEY');
}
validateSignature(payload: string, signature: string): boolean {
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(payload);
const isValid = verifier.verify(
this.stpPublicKey,
signature,
'base64'
);
if (!isValid) {
throw new UnauthorizedException('Firma STP invalida');
}
return true;
}
async processWebhook(
headers: Record<string, string>,
body: STPWebhookPayload
): Promise<void> {
// 1. Validar firma
const signature = headers['x-stp-signature'];
this.validateSignature(JSON.stringify(body), signature);
// 2. Validar timestamp (prevenir replay attacks)
const timestamp = parseInt(headers['x-stp-timestamp']);
const now = Date.now();
if (Math.abs(now - timestamp) > 300000) { // 5 minutos
throw new UnauthorizedException('Timestamp fuera de rango');
}
// 3. Verificar idempotencia
const eventId = headers['x-stp-event-id'];
if (await this.isProcessed(eventId)) {
return; // Ya procesado, ignorar
}
// 4. Procesar segun tipo de evento
await this.handleEvent(body);
}
}
Configuracion de Webhooks en STP
# Configuracion a registrar en panel STP
webhooks:
production:
url: https://api.michangarrito.mx/api/v1/webhooks/stp
events:
- payment.received
- payment.rejected
- refund.completed
authentication:
type: signature
algorithm: RSA-SHA256
retry:
max_attempts: 5
interval: exponential
sandbox:
url: https://staging-api.michangarrito.mx/api/v1/webhooks/stp
events:
- "*"
10. Testing
Modo Sandbox de STP
STP proporciona un ambiente de pruebas completo:
| Ambiente |
URL Base |
Proposito |
| Sandbox |
https://sandbox.stpmex.com/v1 |
Desarrollo y pruebas |
| Produccion |
https://api.stpmex.com/v1 |
Operacion real |
Datos de Prueba
// src/integrations/codi/__tests__/codi.test-data.ts
export const testData = {
// CLABEs de prueba (sandbox STP)
clabes: {
valid: '646180157000000001',
invalid: '000000000000000000',
insufficientFunds: '646180157000000002'
},
// Montos de prueba
amounts: {
success: 100.00,
exceedsLimit: 10000.00,
timeout: 999.99 // Trigger timeout en sandbox
},
// Referencias
references: {
success: 'TEST-SUCCESS-001',
duplicate: 'TEST-DUPLICATE-001',
rejected: 'TEST-REJECTED-001'
}
};
Comandos de Verificacion
# Verificar conectividad con STP sandbox
curl -X GET https://sandbox.stpmex.com/v1/health \
-H "Authorization: Bearer $STP_API_KEY"
# Generar QR de prueba
curl -X POST https://sandbox.stpmex.com/v1/codi/cobros \
-H "Authorization: Bearer $STP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"monto": 100.00,
"concepto": "Prueba MiChangarrito",
"clabe": "646180157000000001",
"referencia": "TEST-001"
}'
# Consultar estado de cobro
curl -X GET https://sandbox.stpmex.com/v1/codi/cobros/TEST-001 \
-H "Authorization: Bearer $STP_API_KEY"
# Simular webhook de pago recibido (solo sandbox)
curl -X POST https://sandbox.stpmex.com/v1/test/simulate-payment \
-H "Authorization: Bearer $STP_API_KEY" \
-d '{"referencia": "TEST-001", "status": "completed"}'
Tests Automatizados
// src/integrations/codi/__tests__/codi.service.spec.ts
describe('CodiService', () => {
describe('generateQR', () => {
it('should generate valid CoDi QR', async () => {
const result = await codiService.generateQR({
monto: 100.00,
concepto: 'Test payment',
referencia: 'TEST-001'
});
expect(result.qrCode).toBeDefined();
expect(result.expiresAt).toBeInstanceOf(Date);
expect(result.referencia).toBe('TEST-001');
});
it('should reject amount over CoDi limit', async () => {
await expect(codiService.generateQR({
monto: 10000.00, // Excede $8,000
concepto: 'Test',
referencia: 'TEST-002'
})).rejects.toThrow('CODI_002');
});
});
describe('webhook validation', () => {
it('should validate STP signature', async () => {
const validPayload = signWithTestKey(testPayload);
const result = await webhookValidator.validateSignature(
testPayload,
validPayload.signature
);
expect(result).toBe(true);
});
it('should reject invalid signature', async () => {
await expect(webhookValidator.validateSignature(
testPayload,
'invalid-signature'
)).rejects.toThrow(UnauthorizedException);
});
});
});
11. Monitoreo
Metricas Especificas
| Metrica |
Descripcion |
Alerta |
codi.qr.generated |
QRs generados por minuto |
> 100/min warning |
codi.payment.confirmed |
Pagos confirmados |
- |
codi.payment.confirmation_time |
Tiempo entre QR y pago |
> 120s warning |
codi.payment.success_rate |
Tasa de exito de pagos |
< 95% critical |
codi.webhook.received |
Webhooks recibidos |
- |
codi.webhook.validation_failed |
Firmas invalidas |
> 0 critical |
stp.api.latency |
Latencia API STP |
> 2s warning |
stp.api.errors |
Errores de API |
> 5/min critical |
Logs Estructurados
// src/integrations/codi/codi.logger.ts
import { Logger } from '@nestjs/common';
export class CodiLogger {
private readonly logger = new Logger('CoDi');
logQRGenerated(data: {
tenantId: string;
referencia: string;
monto: number;
clabe: string;
}) {
this.logger.log({
event: 'codi.qr.generated',
tenantId: data.tenantId,
referencia: data.referencia,
monto: data.monto,
clabe: this.maskClabe(data.clabe),
timestamp: new Date().toISOString()
});
}
logPaymentReceived(data: {
tenantId: string;
referencia: string;
monto: number;
ordenante: string;
confirmationTime: number;
}) {
this.logger.log({
event: 'codi.payment.received',
tenantId: data.tenantId,
referencia: data.referencia,
monto: data.monto,
ordenante: this.maskName(data.ordenante),
confirmationTimeMs: data.confirmationTime,
timestamp: new Date().toISOString()
});
}
logWebhookError(data: {
eventId: string;
error: string;
signature: string;
}) {
this.logger.error({
event: 'codi.webhook.validation_failed',
eventId: data.eventId,
error: data.error,
signaturePrefix: data.signature.substring(0, 20),
timestamp: new Date().toISOString()
});
}
private maskClabe(clabe: string): string {
return clabe.substring(0, 6) + '******' + clabe.substring(12);
}
private maskName(name: string): string {
return name.substring(0, 2) + '***';
}
}
Dashboard Recomendado
# Grafana dashboard config
panels:
- title: "Pagos CoDi - Tiempo Real"
type: stat
query: "sum(rate(codi_payment_confirmed_total[5m]))"
- title: "Tiempo de Confirmacion"
type: gauge
query: "histogram_quantile(0.95, codi_payment_confirmation_seconds)"
thresholds:
- value: 30
color: green
- value: 60
color: yellow
- value: 120
color: red
- title: "Tasa de Exito"
type: gauge
query: "codi_payment_success_rate"
thresholds:
- value: 99
color: green
- value: 95
color: yellow
- value: 0
color: red
12. Referencias
Documentacion Oficial
Modulos Relacionados (MiChangarrito)
| Modulo |
Descripcion |
Relacion |
MOD-003 |
Pagos |
Modulo padre de integraciones de pago |
INT-001 |
Stripe |
Alternativa para pagos con tarjeta |
INT-007 |
Facturacion SAT |
Generacion de CFDI post-pago |
SEC-002 |
Encriptacion |
Almacenamiento seguro de credenciales |
Normativas Aplicables
- Ley Fintech: Regulacion de instituciones de tecnologia financiera
- Circular 4/2019 Banxico: Reglas de operacion CoDi
- PCI DSS: Aunque CoDi no maneja tarjetas, aplica para datos sensibles
- LFPDPPP: Proteccion de datos personales de ordenantes
13. Notas de Implementacion
- CoDi no cobra comisiones por transaccion (beneficio vs tarjetas)
- Las transferencias SPEI son irreversibles una vez confirmadas
- Implementar timeout de 5 minutos para codigos QR
- Validar firma digital en todas las notificaciones de STP
- El registro como participante CoDi requiere tramite con Banxico
- Generar referencia unica por transaccion para conciliacion
- Los QR deben cumplir con el estandar EMVCo para CoDi
- Horario de operacion SPEI: 24/7, pero cortes contables a las 17:30
- Considerar implementar cache de CLABEs validadas
- Manejar reintentos en caso de falla de comunicacion con STP