En resumen: El sondeo busca actualizaciones periódicamente (sencillo pero ineficiente). Los webhooks envían actualizaciones en tiempo real (eficiente pero complejo). Usa el sondeo para verificaciones infrecuentes, y los webhooks para actualizaciones en tiempo real. La Modern PetstoreAPI soporta ambos patrones con entrega confiable de webhooks.
Comprendiendo la Diferencia
Sondeo (Polling): El cliente pregunta "¿Hay actualizaciones?" repetidamente. Webhooks: El servidor dice "¡Aquí hay una actualización!" cuando algo sucede.
Analogía:
- Sondeo = Revisar tu buzón cada hora
- Webhooks = El cartero toca el timbre cuando llega el correo
Sondeo: Cómo Funciona
El cliente realiza solicitudes periódicas para verificar cambios.
// Sondea cada 30 segundos
setInterval(async () => {
const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
const order = await response.json();
if (order.status === 'completed') {
console.log('Order completed!', order);
clearInterval(pollInterval);
}
}, 30000);
Patrones de sondeo:
Sondeo simple:
GET /api/v1/orders/123
# Devuelve el estado actual del pedido
Sondeo condicional (ETag):
GET /api/v1/orders/123
If-None-Match: "abc123"
# Devuelve 304 Not Modified si no hay cambios
# Devuelve 200 con nuevos datos si hay cambios
Sondeo basado en "desde":
GET /api/v1/orders/123/events?since=1710331200
# Devuelve eventos desde la marca de tiempo
Webhooks: Cómo Funcionan
El servidor envía una solicitud HTTP POST a tu endpoint cuando ocurren eventos.
Flujo de configuración:
// 1. Registra el endpoint del webhook
POST /api/v1/webhooks
{
"url": "https://myapp.com/webhooks/petstore",
"events": ["order.created", "order.completed"],
"secret": "whsec_abc123"
}
// 2. El servidor envía el webhook cuando ocurre el evento
POST https://myapp.com/webhooks/petstore
{
"id": "evt_123",
"type": "order.completed",
"created": 1710331200,
"data": {
"orderId": "123",
"status": "completed",
"completedAt": "2024-01-01T12:00:00Z"
}
}
// 3. Verifica y procesa el webhook
// Responde con 200 OK
Cuándo Usar el Sondeo
Bueno para:
- Verificaciones infrecuentes (una vez por hora)
- Un pequeño número de recursos
- Implementaciones simples
- Cuando controlas el cliente
- Pruebas y depuración
Ejemplos:
- Verificar el estado de informes diarios
- Sincronizar contactos cada pocos minutos
- Monitorizar la salud del servidor
- Verificar el estado de pago (infrecuente)
El sondeo está bien cuando:
- Las actualizaciones son raras
- Un ligero retraso es aceptable
- Quieres una implementación simple
- El recurso es pequeño
Cuándo Usar Webhooks
Bueno para:
- Actualizaciones en tiempo real
- Muchos recursos a monitorizar
- Eventos sensibles al tiempo
- Integraciones de terceros
- Actualizaciones de alta frecuencia
Ejemplos:
- Confirmaciones de pago
- Mensajes de chat
- Alertas de precios de acciones
- Cambios de estado de pedidos
- Notificaciones de CI/CD
Los webhooks son mejores cuando:
- Las actualizaciones necesitan ser inmediatas
- El sondeo sería ineficiente
- Muchos clientes monitorizando el mismo recurso
- Quieres reducir la carga del servidor
Tabla Comparativa
| Factor | Sondeo | Webhooks |
|---|---|---|
| Latencia | Hasta el intervalo de sondeo | Tiempo real |
| Carga del servidor | Alta (muchas solicitudes vacías) | Baja (solo eventos reales) |
| Complejidad | Simple | Complejo |
| Fiabilidad | Alta (el cliente controla el reintento) | Media (necesita lógica de reintento) |
| Configuración | Ninguna | Registro de endpoint |
| Problemas de firewall | Ninguno (solo saliente) | Puede necesitar incluir en lista blanca |
| Costo | Mayor (más solicitudes) | Menor (menos solicitudes) |
| Mejor para | Verificaciones infrecuentes | Actualizaciones en tiempo real |
Implementando el Sondeo
Sondeo Básico
async function pollOrderStatus(orderId, callback) {
let lastStatus = null;
const poll = async () => {
try {
const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
const order = await response.json();
// Solo llamar al callback si el estado ha cambiado
if (order.status !== lastStatus) {
lastStatus = order.status;
callback(order);
}
// Detener el sondeo si el estado es terminal
if (['completed', 'cancelled'].includes(order.status)) {
return;
}
// Continuar sondeando
setTimeout(poll, 5000);
} catch (error) {
console.error('Polling error:', error);
setTimeout(poll, 30000); // Esperar más en caso de error
}
};
poll();
}
// Uso
pollOrderStatus('order-123', (order) => {
console.log(`Order status: ${order.status}`);
});
Sondeo Inteligente (Retroceso Exponencial)
async function smartPoll(url, callback, options = {}) {
const {
maxRetries = 10,
initialInterval = 1000,
maxInterval = 60000,
stopCondition = () => false
} = options;
let retries = 0;
let interval = initialInterval;
let lastData = null;
const poll = async () => {
try {
const response = await fetch(url);
const data = await response.json();
// Llamar al callback si los datos cambiaron
if (JSON.stringify(data) !== JSON.stringify(lastData)) {
lastData = data;
callback(data);
}
// Detener si se cumple la condición
if (stopCondition(data)) {
return;
}
// Reiniciar el intervalo en solicitudes exitosas
interval = initialInterval;
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error('Max retries exceeded');
}
}
// Programar la siguiente consulta con retroceso exponencial
setTimeout(poll, interval);
interval = Math.min(interval * 2, maxInterval);
};
poll();
}
// Uso: Sondea un pedido hasta que se complete
smartPoll('https://petstoreapi.com/api/v1/orders/123',
(order) => console.log('Order:', order),
{
stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
initialInterval: 2000,
maxInterval: 30000
}
);
Sondeo con ETag
async function pollWithEtag(url, callback) {
let etag = null;
const poll = async () => {
const headers = {};
if (etag) {
headers['If-None-Match'] = etag;
}
const response = await fetch(url, { headers });
if (response.status === 304) {
// No modificado, continuar sondeando
setTimeout(poll, 30000);
return;
}
const data = await response.json();
etag = response.headers.get('etag');
callback(data);
setTimeout(poll, 30000);
};
poll();
}
Implementando Webhooks
Registrando Webhooks
// Registrar el endpoint del webhook
async function registerWebhook(url, events) {
const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
url,
events,
secret: generateSecret()
})
});
return response.json();
}
function generateSecret() {
return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
Recibiendo Webhooks
const express = require('express');
const crypto = require('crypto');
const app = express();
// Analizador de cuerpo crudo para la verificación de firma
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/petstore', async (req, res) => {
const signature = req.headers['x-petstore-signature'];
const body = req.body;
// Verificar firma
const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(body.toString());
// Procesar evento
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
case 'order.completed':
await handleOrderCompleted(event.data);
break;
case 'order.cancelled':
await handleOrderCancelled(event.data);
break;
}
// Confirmar recepción
res.status(200).json({ received: true });
});
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Probando Webhooks Localmente
# Usa ngrok para exponer el endpoint local
ngrok http 3000
# Registra la URL de ngrok como endpoint de webhook
curl -X POST https://petstoreapi.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/petstore",
"events": ["order.created", "order.completed"]
}'
Entrega Confiable de Webhooks
Los webhooks pueden fallar. Implementa lógica de reintento.
Lado del Remitente (Servidor)
// Encola webhooks para su entrega
const webhookQueue = [];
async function sendWebhook(event) {
const webhooks = await db.webhooks.findMany({
where: { events: { contains: event.type } }
});
for (const webhook of webhooks) {
webhookQueue.push({
webhook,
event,
attempts: 0,
nextAttempt: Date.now()
});
}
processQueue();
}
async function processQueue() {
const now = Date.now();
for (const item of webhookQueue) {
if (item.nextAttempt > now) continue;
try {
await deliverWebhook(item);
// Eliminar de la cola al tener éxito
webhookQueue.splice(webhookQueue.indexOf(item), 1);
} catch (error) {
// Programar reintento con retroceso exponencial
item.attempts++;
item.nextAttempt = now + getBackoff(item.attempts);
if (item.attempts >= 5) {
// Marcar como fallido después de 5 intentos
await markWebhookFailed(item);
webhookQueue.splice(webhookQueue.indexOf(item), 1);
}
}
}
setTimeout(processQueue, 5000);
}
function getBackoff(attempt) {
// 1min, 5min, 15min, 1hr, 4hr
const delays = [60000, 300000, 900000, 3600000, 14400000];
return delays[attempt - 1] || delays[delays.length - 1];
}
async function deliverWebhook({ webhook, event }) {
const signature = generateSignature(event, webhook.secret);
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Petstore-Signature': signature,
'X-Petstore-Event': event.type
},
body: JSON.stringify(event),
timeout: 10000
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
}
Lado del Receptor (Cliente)
// Manejo de webhook idempotente
const processedEvents = new Set();
app.post('/webhooks/petstore', async (req, res) => {
const event = JSON.parse(req.body.toString());
// Omitir si ya se procesó (idempotencia)
if (processedEvents.has(event.id)) {
return res.status(200).json({ received: true });
}
try {
await processEvent(event);
processedEvents.add(event.id);
// Limpiar IDs de eventos antiguos (mantener los últimos 1000)
if (processedEvents.size > 1000) {
const arr = Array.from(processedEvents);
arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Devolver 5xx para activar el reintento
res.status(500).json({ error: 'Processing failed' });
}
});
async function processEvent(event) {
// Procesar el evento
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
// ... manejar otros eventos
}
}
Enfoque Híbrido
Usa tanto el sondeo como los webhooks para actualizaciones críticas.
class OrderMonitor {
constructor(orderId, callback) {
this.orderId = orderId;
this.callback = callback;
this.pollInterval = null;
}
async start() {
// Comenzar con sondeo para retroalimentación inmediata
this.startPolling();
// Registrar webhook para actualización en tiempo real
await this.registerWebhook();
}
startPolling() {
this.pollInterval = setInterval(async () => {
const order = await this.fetchOrder();
this.callback(order);
if (['completed', 'cancelled'].includes(order.status)) {
this.stop();
}
}, 10000);
}
async registerWebhook() {
const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
method: 'POST',
headers: { 'Authorization': `Bearer ${TOKEN}` },
body: JSON.stringify({
url: 'https://myapp.com/webhooks/petstore',
events: [`order.${this.orderId}`],
oneTime: true // Auto-eliminar después de la primera entrega
})
});
this.webhookId = (await response.json()).id;
}
stop() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
if (this.webhookId) {
fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
method: 'DELETE'
});
}
}
}
Preguntas Frecuentes
P: ¿Con qué frecuencia debo sondear? Depende de la urgencia. 30 segundos para casi tiempo real. 5 minutos para no urgentes. Equilibra la frescura con la carga del servidor.
P: ¿Qué pasa si mi endpoint de webhook está caído? Los buenos proveedores de webhooks reintentan con retroceso exponencial. Implementa la idempotencia para manejar entregas duplicadas.
P: ¿Cómo aseguro los webhooks? Verifica las firmas usando secretos compartidos. Usa solo HTTPS. Valida los datos del evento.
P: ¿Puedo usar webhooks para datos históricos? No. Los webhooks son solo para eventos nuevos. Usa el sondeo o las APIs por lotes para datos históricos.
P: ¿Debo usar sondeo o webhooks para aplicaciones móviles? El sondeo es más simple para móviles. Los webhooks requieren notificaciones push como intermediario.
P: ¿Cómo depuro problemas de webhook? Usa herramientas como webhook.site para pruebas. Registra todas las entregas de webhooks. Proporciona un historial de eventos de webhook en tu API.
Modern PetstoreAPI soporta tanto el sondeo como los webhooks. Consulta la guía de webhooks para detalles de implementación. Prueba las integraciones de webhook con Apidog.
Top comments (0)