638 lines
20 KiB
Markdown
638 lines
20 KiB
Markdown
# Integración Bold — Guía completa
|
|
|
|
Guía de referencia para integrar Bold (pasarela de pagos colombiana) en cualquier sistema backend.
|
|
Basada en experiencia real de producción con PHP; los conceptos aplican a cualquier lenguaje.
|
|
|
|
---
|
|
|
|
## Índice
|
|
|
|
1. [Credenciales y modos](#1-credenciales-y-modos)
|
|
2. [Crear un Payment Link (API)](#2-crear-un-payment-link-api)
|
|
3. [Consultar estado de un link](#3-consultar-estado-de-un-link)
|
|
4. [Callback URL (retorno del usuario)](#4-callback-url-retorno-del-usuario)
|
|
5. [Webhook — configuración](#5-webhook--configuración)
|
|
6. [Webhook — verificación de firma](#6-webhook--verificación-de-firma)
|
|
7. [Webhook — procesamiento del evento](#7-webhook--procesamiento-del-evento)
|
|
8. [Estrategia de referencias múltiples](#8-estrategia-de-referencias-múltiples)
|
|
9. [Idempotencia](#9-idempotencia)
|
|
10. [Esquema de base de datos](#10-esquema-de-base-de-datos)
|
|
11. [Flujo completo de extremo a extremo](#11-flujo-completo-de-extremo-a-extremo)
|
|
12. [Errores frecuentes y soluciones](#12-errores-frecuentes-y-soluciones)
|
|
|
|
---
|
|
|
|
## 1. Credenciales y modos
|
|
|
|
Bold maneja dos entornos que se diferencian **únicamente por la API key**; el endpoint es el mismo.
|
|
|
|
| Parámetro | Descripción |
|
|
|--------------------|-----------------------------------------------------------|
|
|
| `bold_api_key` | Clave de la API (diferente para test y producción) |
|
|
| `bold_secret_key` | Clave secreta para verificar la firma del webhook |
|
|
| `bold_mode` | `"test"` / `"production"` (para lógica propia del sistema)|
|
|
|
|
> **Importante:** En modo **test** la clave secreta del webhook es una cadena vacía `""`.
|
|
> En producción se usa la clave real del panel de Bold.
|
|
|
|
### Obtener las credenciales
|
|
|
|
1. Ingresa al panel de Bold → **Configuración → Integraciones → API**.
|
|
2. Copia la **API Key** y la **Secret Key**.
|
|
3. Para el webhook, registra tu URL en **Configuración → Webhooks**.
|
|
|
|
---
|
|
|
|
## 2. Crear un Payment Link (API)
|
|
|
|
### Endpoint
|
|
|
|
```
|
|
POST https://integrations.api.bold.co/online/link/v1
|
|
```
|
|
|
|
### Headers
|
|
|
|
```
|
|
Authorization: x-api-key {bold_api_key}
|
|
Content-Type: application/json
|
|
Accept: application/json
|
|
```
|
|
|
|
### Payload JSON
|
|
|
|
```json
|
|
{
|
|
"amount_type": "CLOSE",
|
|
"amount": {
|
|
"currency": "COP",
|
|
"total_amount": 25000
|
|
},
|
|
"description": "Descripción del producto o servicio",
|
|
"callback_url": "https://tudominio.com/pago_exitoso.php",
|
|
"payer_email": "cliente@ejemplo.com",
|
|
"reference": "TU-REF-12345-1746123456"
|
|
}
|
|
```
|
|
|
|
> **Moneda COP:** Bold no usa centavos. `total_amount: 25000` equivale a $25.000 pesos.
|
|
|
|
#### Campos clave
|
|
|
|
| Campo | Tipo | Descripción |
|
|
|----------------|---------|-----------------------------------------------------------------------------|
|
|
| `amount_type` | string | `"CLOSE"` = monto fijo. `"OPEN"` = el usuario puede ingresar el monto. |
|
|
| `total_amount` | int | Valor en pesos COP (sin centavos). |
|
|
| `callback_url` | string | URL a la que Bold redirige al usuario tras el pago. |
|
|
| `payer_email` | string | Pre-rellena el email en la pasarela. |
|
|
| `reference` | string | **Tu referencia personalizada.** Llega al webhook. Max 60 chars, alfanumérico + guiones. |
|
|
|
|
### Respuesta exitosa (`HTTP 200`)
|
|
|
|
```json
|
|
{
|
|
"payload": {
|
|
"payment_link": "LNK_abc123xyz",
|
|
"url": "https://checkout.bold.co/payment/LNK_abc123xyz"
|
|
}
|
|
}
|
|
```
|
|
|
|
- `payment_link` → ID interno de Bold (formato `LNK_xxx`). Guardarlo en tu base de datos.
|
|
- `url` → URL a la que debes redirigir al usuario.
|
|
|
|
### Ejemplo en PHP
|
|
|
|
```php
|
|
$payload = [
|
|
'amount_type' => 'CLOSE',
|
|
'amount' => ['currency' => 'COP', 'total_amount' => 25000],
|
|
'description' => 'Inscripción al evento',
|
|
'callback_url' => 'https://tudominio.com/pago_exitoso.php',
|
|
'payer_email' => $email,
|
|
'reference' => 'ERA-' . $usuarioId . '-' . time(),
|
|
];
|
|
|
|
$ch = curl_init('https://integrations.api.bold.co/online/link/v1');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
|
CURLOPT_HTTPHEADER => [
|
|
'Authorization: x-api-key ' . $apiKey,
|
|
'Content-Type: application/json',
|
|
],
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200) {
|
|
// Manejar error
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
$linkId = $data['payload']['payment_link'];
|
|
$paymentUrl = $data['payload']['url'];
|
|
|
|
header('Location: ' . $paymentUrl);
|
|
exit;
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Consultar estado de un link
|
|
|
|
Útil si el webhook no llegó o como verificación extra en el callback.
|
|
|
|
### Endpoint
|
|
|
|
```
|
|
GET https://integrations.api.bold.co/online/link/v1/{payment_link}
|
|
```
|
|
|
|
### Headers
|
|
|
|
```
|
|
Authorization: x-api-key {bold_api_key}
|
|
Accept: application/json
|
|
```
|
|
|
|
### Respuesta relevante
|
|
|
|
```json
|
|
{
|
|
"payload": {
|
|
"payment_link": "LNK_abc123xyz",
|
|
"status": "ACTIVE",
|
|
"transactions": [
|
|
{
|
|
"status": "APPROVED",
|
|
"payment_method": "CARD",
|
|
"amount": 25000
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Callback URL (retorno del usuario)
|
|
|
|
Cuando el usuario termina (o abandona) el pago, Bold lo redirige a tu `callback_url` con parámetros GET.
|
|
|
|
> **Advertencia:** Bold puede usar distintos nombres de parámetro según la versión del checkout.
|
|
> **Nunca confíes en un único parámetro.** Revisa todos los posibles:
|
|
|
|
```
|
|
bold-order-id, order_id, payment_link, payment_link_id, reference, id
|
|
```
|
|
|
|
### Estrategia robusta en PHP
|
|
|
|
```php
|
|
// Recolectar todos los candidatos posibles
|
|
$candidateRefs = [];
|
|
foreach (['bold-order-id', 'order_id', 'payment_link', 'reference', 'id'] as $key) {
|
|
$val = trim((string)($_GET[$key] ?? ''));
|
|
if ($val !== '') {
|
|
$candidateRefs[] = $val;
|
|
}
|
|
}
|
|
// Agregar lo guardado en sesión antes de la redirección
|
|
if (!empty($_SESSION['bold_order_id'])) {
|
|
$candidateRefs[] = $_SESSION['bold_order_id'];
|
|
}
|
|
$candidateRefs = array_values(array_unique(array_filter($candidateRefs)));
|
|
|
|
// Buscar usuario por cualquiera de las referencias
|
|
$usuario = null;
|
|
foreach ($candidateRefs as $ref) {
|
|
$usuarioId = buscarUsuarioIdPorReferencia($pdo, $ref);
|
|
if ($usuarioId) {
|
|
// Cargar el usuario y salir del loop
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Respaldo: usar ID de sesión si no se encontró por referencia
|
|
if (!$usuario && !empty($_SESSION['usuario_id'])) {
|
|
// Cargar por $_SESSION['usuario_id']
|
|
}
|
|
```
|
|
|
|
> **Nota crítica:** La callback se dispara **antes** del webhook. No es seguro marcar un pago
|
|
> como aprobado solo por el callback; el origen de verdad es el **webhook**.
|
|
> En el callback solo confirma que el usuario regresó y muestra una pantalla de "procesando".
|
|
|
|
---
|
|
|
|
## 5. Webhook — configuración
|
|
|
|
### Registrar la URL en Bold
|
|
|
|
Panel de Bold → **Configuración → Webhooks** → agregar URL:
|
|
|
|
```
|
|
https://tudominio.com/webhook.php
|
|
```
|
|
|
|
### Eventos disponibles
|
|
|
|
| Evento | Descripción |
|
|
|------------------|------------------------------------|
|
|
| `SALE_APPROVED` | Pago aprobado exitosamente |
|
|
| `SALE_REJECTED` | Pago rechazado por la entidad |
|
|
| `SALE_REVERSED` | Reverso / devolución |
|
|
| `CHARGEBACK` | Contracargo iniciado |
|
|
|
|
Para la mayoría de casos solo necesitas `SALE_APPROVED`.
|
|
|
|
### Requisitos del receptor
|
|
|
|
- Debe responder **HTTP 200** en **menos de 2 segundos**.
|
|
- Si la respuesta demora más, Bold reintentará el envío.
|
|
- La URL debe ser HTTPS en producción.
|
|
|
|
---
|
|
|
|
## 6. Webhook — verificación de firma
|
|
|
|
Bold firma cada notificación con HMAC-SHA256. El header es `x-bold-signature`.
|
|
|
|
### Algoritmo de verificación
|
|
|
|
```
|
|
1. Leer el body RAW (antes de hacer cualquier json_decode)
|
|
2. Codificar en Base64: encoded = base64(rawBody)
|
|
3. Calcular HMAC: computed = HMAC-SHA256(encoded, secretKey) → hexadecimal
|
|
4. Comparar con el header x-bold-signature (timing-safe)
|
|
```
|
|
|
|
> **Modo test:** la `secretKey` es `""` (cadena vacía).
|
|
> **Modo producción:** usa la clave del panel de Bold.
|
|
|
|
### Implementación PHP
|
|
|
|
```php
|
|
$rawBody = (string) file_get_contents('php://input');
|
|
$signatureHeader = $_SERVER['HTTP_X_BOLD_SIGNATURE'] ?? '';
|
|
|
|
// Responder 200 ANTES de todo procesamiento
|
|
http_response_code(200);
|
|
header('Content-Type: application/json');
|
|
header('Connection: close');
|
|
$body = '{"ok":true}';
|
|
header('Content-Length: ' . strlen($body));
|
|
echo $body;
|
|
if (function_exists('fastcgi_finish_request')) {
|
|
fastcgi_finish_request();
|
|
} else {
|
|
flush();
|
|
}
|
|
|
|
// --- A partir de aquí el cliente ya recibió 200 ---
|
|
|
|
if ($signatureHeader !== '') {
|
|
$keyForHmac = ($boldMode === 'production') ? $secretKey : '';
|
|
$encoded = base64_encode($rawBody);
|
|
$computed = hash_hmac('sha256', $encoded, $keyForHmac);
|
|
|
|
if (!hash_equals($computed, $signatureHeader)) {
|
|
// Firma inválida: loguear y salir
|
|
exit;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Webhook — procesamiento del evento
|
|
|
|
### Estructura del payload JSON
|
|
|
|
```json
|
|
{
|
|
"id": "notif_uuid_unico",
|
|
"type": "SALE_APPROVED",
|
|
"subject": "TXN_xyz789",
|
|
"data": {
|
|
"payment_id": "TXN_xyz789",
|
|
"amount": {
|
|
"total": 25000,
|
|
"currency": "COP"
|
|
},
|
|
"payer_email": "cliente@ejemplo.com",
|
|
"metadata": {
|
|
"reference": "ERA-42-1746123456"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Campos a extraer
|
|
|
|
| Campo | Ruta en JSON | Descripción |
|
|
|----------------------------|---------------------------------------------|------------------------------------------|
|
|
| `notification_id` | `event.id` | ID único de la notificación (idempotencia) |
|
|
| `tipo` | `event.type` | Tipo de evento |
|
|
| `payment_id` | `event.data.payment_id` o `event.subject` | ID de la transacción Bold |
|
|
| `referencia` | `event.data.metadata.reference` | Tu referencia personalizada |
|
|
| `payer_email` | `event.data.payer_email` | Email del pagador |
|
|
| `amount` | `event.data.amount.total` | Monto en pesos COP |
|
|
|
|
### Flujo de procesamiento
|
|
|
|
```php
|
|
$event = json_decode($rawBody, true);
|
|
$notificationId = $event['id'] ?? '';
|
|
$tipo = $event['type'] ?? '';
|
|
$data = $event['data'] ?? [];
|
|
|
|
$paymentId = $data['payment_id'] ?? ($event['subject'] ?? '');
|
|
$referencia = $data['metadata']['reference'] ?? '';
|
|
$email = $data['payer_email'] ?? '';
|
|
|
|
// Solo procesar ventas aprobadas
|
|
if ($tipo !== 'SALE_APPROVED') {
|
|
exit;
|
|
}
|
|
|
|
// Verificar idempotencia (ver sección 9)
|
|
// Resolver usuario (ver sección 8)
|
|
// Marcar pago en la base de datos
|
|
// Enviar correo de confirmación
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Estrategia de referencias múltiples
|
|
|
|
**Problema real:** Bold puede enviar en el webhook un `payment_id` diferente al `LNK_xxx`
|
|
original del payment link. Si solo guardas `bold_order_id`, la búsqueda falla.
|
|
|
|
**Solución:** tabla de historial de referencias. Cada vez que aparece una referencia nueva
|
|
asociada a un usuario, se registra.
|
|
|
|
### Tabla `usuarios_referencias_pago`
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS `usuarios_referencias_pago` (
|
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
`usuario_id` INT NOT NULL,
|
|
`referencia` VARCHAR(120) NOT NULL,
|
|
`tipo` VARCHAR(40) NOT NULL DEFAULT 'desconocida',
|
|
`origen` VARCHAR(40) NOT NULL DEFAULT 'sistema',
|
|
`creado_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE KEY `uq_referencia` (`referencia`),
|
|
INDEX `idx_usuario` (`usuario_id`),
|
|
CONSTRAINT `fk_ref_usuario`
|
|
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
```
|
|
|
|
### Cuándo registrar referencias
|
|
|
|
| Momento | Referencias a guardar | `tipo` |
|
|
|----------------------|--------------------------------------------------------|------------------|
|
|
| Al crear el link | `LNK_xxx` (payment_link) | `payment_link` |
|
|
| Al crear el link | `ERA-{id}-{ts}` (tu referencia) | `referencia_era` |
|
|
| En el callback URL | Todos los params GET de Bold | `callback` |
|
|
| En el webhook | `payment_id` del evento | `webhook_payment_id` |
|
|
| En el webhook | `metadata.reference` del evento | `webhook_referencia` |
|
|
|
|
### Función de búsqueda por cualquier referencia
|
|
|
|
```php
|
|
function buscarUsuarioIdPorReferencia(PDO $pdo, string $referencia): ?int {
|
|
$stmt = $pdo->prepare('
|
|
SELECT usuario_id
|
|
FROM usuarios_referencias_pago
|
|
WHERE referencia = ?
|
|
LIMIT 1
|
|
');
|
|
$stmt->execute([trim($referencia)]);
|
|
$row = $stmt->fetchColumn();
|
|
return $row ? (int)$row : null;
|
|
}
|
|
```
|
|
|
|
### Cascada de búsqueda en el webhook
|
|
|
|
```php
|
|
$usuarioId = null;
|
|
|
|
// 1. Por referencia personalizada ERA-{id}-{ts}
|
|
if ($referencia !== '') {
|
|
$usuarioId = buscarUsuarioIdPorReferencia($pdo, $referencia);
|
|
}
|
|
|
|
// 2. Por payment_id de la transacción
|
|
if (!$usuarioId && $paymentId !== '') {
|
|
$usuarioId = buscarUsuarioIdPorReferencia($pdo, $paymentId);
|
|
}
|
|
|
|
// 3. Por email (último recurso, solo si hay un único pendiente)
|
|
if (!$usuarioId && $payerEmail !== '') {
|
|
$stmt = $pdo->prepare('
|
|
SELECT id FROM usuarios
|
|
WHERE email = ? AND estado_pago = "pendiente"
|
|
ORDER BY fecha_registro DESC LIMIT 1
|
|
');
|
|
$stmt->execute([$payerEmail]);
|
|
$row = $stmt->fetchColumn();
|
|
$usuarioId = $row ? (int)$row : null;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Idempotencia
|
|
|
|
Bold puede reenviar la misma notificación varias veces (reintentos ante timeouts).
|
|
Debes garantizar que procesar la misma notificación dos veces no cause efectos duplicados
|
|
(doble correo, doble registro de pago, etc.).
|
|
|
|
### Tabla `webhook_log`
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS `webhook_log` (
|
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
`notification_id` VARCHAR(64) NOT NULL,
|
|
`tipo` VARCHAR(30) NOT NULL,
|
|
`payment_id` VARCHAR(64) DEFAULT NULL,
|
|
`referencia` VARCHAR(120) DEFAULT NULL,
|
|
`usuario_id` INT DEFAULT NULL,
|
|
`procesado` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`recibido_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE KEY `uq_notification` (`notification_id`),
|
|
INDEX `idx_usuario` (`usuario_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
```
|
|
|
|
### Patrón de INSERT IGNORE
|
|
|
|
```php
|
|
// Intentar insertar. Si ya existe (UNIQUE conflict) → rowCount() = 0 → duplicado
|
|
$ins = $pdo->prepare('
|
|
INSERT IGNORE INTO webhook_log (notification_id, tipo, payment_id, referencia)
|
|
VALUES (?, ?, ?, ?)
|
|
');
|
|
$ins->execute([$notificationId, $tipo, $paymentId, $referencia]);
|
|
|
|
if ($ins->rowCount() === 0) {
|
|
// Notificación ya procesada anteriormente
|
|
exit;
|
|
}
|
|
|
|
// Continúa procesamiento...
|
|
|
|
// Al finalizar, marcar como procesado
|
|
$pdo->prepare('UPDATE webhook_log SET procesado=1, usuario_id=? WHERE notification_id=?')
|
|
->execute([$usuarioId, $notificationId]);
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Esquema de base de datos
|
|
|
|
Tablas mínimas necesarias:
|
|
|
|
```sql
|
|
-- Tabla principal de usuarios/registros
|
|
CREATE TABLE `usuarios` (
|
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
`email` VARCHAR(255) NOT NULL,
|
|
`token` VARCHAR(64) NOT NULL UNIQUE,
|
|
`bold_order_id` VARCHAR(80) DEFAULT NULL, -- LNK_xxx principal
|
|
`estado_pago` ENUM('pendiente','pagado') NOT NULL DEFAULT 'pendiente',
|
|
`fecha_registro` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
|
|
-- Historial de todas las referencias de Bold asociadas al usuario
|
|
CREATE TABLE `usuarios_referencias_pago` (
|
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
`usuario_id` INT NOT NULL,
|
|
`referencia` VARCHAR(120) NOT NULL,
|
|
`tipo` VARCHAR(40) NOT NULL DEFAULT 'desconocida',
|
|
`origen` VARCHAR(40) NOT NULL DEFAULT 'sistema',
|
|
`creado_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE KEY `uq_referencia` (`referencia`),
|
|
INDEX `idx_usuario` (`usuario_id`),
|
|
CONSTRAINT `fk_ref_usuario`
|
|
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
|
|
-- Log de webhooks para idempotencia
|
|
CREATE TABLE `webhook_log` (
|
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
`notification_id` VARCHAR(64) NOT NULL,
|
|
`tipo` VARCHAR(30) NOT NULL,
|
|
`payment_id` VARCHAR(64) DEFAULT NULL,
|
|
`referencia` VARCHAR(120) DEFAULT NULL,
|
|
`usuario_id` INT DEFAULT NULL,
|
|
`procesado` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`recibido_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE KEY `uq_notification` (`notification_id`),
|
|
INDEX `idx_usuario` (`usuario_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Flujo completo de extremo a extremo
|
|
|
|
```
|
|
Usuario llena formulario
|
|
│
|
|
▼
|
|
[Tu backend] genera referencia personalizada:
|
|
ERA-{usuarioId}-{timestamp}
|
|
│
|
|
▼
|
|
POST → API Bold /online/link/v1
|
|
│
|
|
▼ respuesta LNK_xxx + URL
|
|
[Guardar en DB]:
|
|
- usuarios.bold_order_id = LNK_xxx
|
|
- referencias_pago: LNK_xxx (tipo: payment_link)
|
|
- referencias_pago: ERA-... (tipo: referencia_era)
|
|
- sesión: bold_order_id, usuario_id
|
|
│
|
|
▼
|
|
Redirigir usuario a URL Bold (checkout)
|
|
│
|
|
Usuario paga
|
|
│
|
|
┌────┴────────────────────────────────┐
|
|
│ │
|
|
▼ ▼
|
|
callback_url (GET) webhook (POST) ← fuente de verdad
|
|
Bold redirige al usuario Bold notifica el evento
|
|
con params: bold-order-id, etc.
|
|
│ │
|
|
▼ ▼
|
|
Mostrar pantalla "procesando" 1. Responder 200 INMEDIATAMENTE
|
|
No marcar como pagado aún 2. Verificar firma HMAC
|
|
3. Idempotencia (INSERT IGNORE)
|
|
4. Resolver usuario por referencias
|
|
5. UPDATE estado_pago = 'pagado'
|
|
6. Enviar correo de confirmación
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Errores frecuentes y soluciones
|
|
|
|
### El webhook no llega
|
|
|
|
- Verificar que la URL esté registrada en el panel de Bold.
|
|
- La URL debe ser **pública** (no localhost). Usar [ngrok](https://ngrok.com) para pruebas locales:
|
|
```bash
|
|
ngrok http 80
|
|
# Registrar la URL https://xxxx.ngrok.io/webhook.php en Bold
|
|
```
|
|
- Confirmar que el servidor responde HTTP 200 en < 2 s.
|
|
- Revisar el log de webhooks en el panel de Bold para ver reintentos.
|
|
|
|
### Firma inválida en modo test
|
|
|
|
El `secretKey` en modo test es `""` (cadena vacía). Asegúrate de usar eso, no la clave real.
|
|
|
|
```php
|
|
$keyForHmac = ($boldMode === 'production') ? $secretKey : '';
|
|
```
|
|
|
|
### No se encuentra el usuario en el webhook
|
|
|
|
El `payment_id` que llega en el webhook puede ser **diferente** al `LNK_xxx` creado con la API.
|
|
Solución: usar la tabla de historial de referencias y la referencia personalizada `ERA-{id}-{ts}`.
|
|
|
|
### Bold reintenta y se procesan pagos dobles
|
|
|
|
Implementar idempotencia con `webhook_log` usando `INSERT IGNORE` sobre `notification_id`.
|
|
|
|
### HTTP 400 al crear el link
|
|
|
|
- Verificar que `total_amount` sea entero (no float).
|
|
- `reference` no puede superar 60 caracteres ni contener caracteres especiales.
|
|
- La `callback_url` debe ser una URL accesible públicamente.
|
|
|
|
### Error de timeout en curl
|
|
|
|
Bold tiene un timeout de respuesta. Si el servidor tarda más de 15 s, la llamada falla.
|
|
Configurar `CURLOPT_TIMEOUT` en 15 y asegurar que la conexión a internet es estable.
|
|
|
|
---
|
|
|
|
## Referencias oficiales
|
|
|
|
- Documentación Bold: [https://developers.bold.co](https://developers.bold.co)
|
|
- Panel Bold: [https://dashboard.bold.co](https://dashboard.bold.co)
|
|
- API de links: `POST https://integrations.api.bold.co/online/link/v1`
|
|
- Consultar link: `GET https://integrations.api.bold.co/online/link/v1/{id}`
|