Un webhook es un POST que GREM envía a su URL cuando ocurre algo: termina un trabajo, cambia un inmueble o un lead. Gestionar suscripciones requiere el ámbito webhooks y es gratuito. Los webhooks de una agencia se gestionan con la clave de toda la agencia; una clave de agente recibe 403.
Suscribirse
curl -X POST https://developers.grem.capital/api/v1/webhooks \
-H "Authorization: Bearer gsk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/grem-hook","events":["content.completed","content.failed","lead.created"]}'
{ "id": "...", "url": "https://example.com/grem-hook", "events": [...], "status": "active",
"failureCount": 0, "lastDeliveryAt": null, "secret": "whsec_..." }
url: una dirección públicahttps://en el puerto estándar (443), sinuser:password. Otros puertos, credenciales en la dirección y las direcciones privadas, locales e internas se rechazan (400). La regla del puerto se aplica al fijar una dirección: las direcciones guardadas antes con otro puerto siguen recibiendo entregas.events: al menos un evento de la lista de abajo; los nombres desconocidos se descartan.secret: el secreto de firma. Se devuelve una sola vez: guárdelo.- Hasta 10 direcciones por cuenta y, aparte, hasta 10 direcciones de sandbox creadas con claves de prueba; la 11.ª se rechaza con
422 business_rule. - Las direcciones creadas con una clave de prueba son direcciones del sandbox: reciben solo eventos de trabajos del sandbox (Sandbox).
Las suscripciones también se gestionan en el panel: API → Webhooks. Allí también puede cambiar el secreto de firma.
Gestión
| Solicitud | Qué hace |
|---|---|
GET /webhooks | sus suscripciones (sin secretos) |
PATCH /webhooks/{id} | cambiar url, events o status (active / disabled) |
DELETE /webhooks/{id} | eliminar la suscripción |
POST /webhooks/{id}/test | enviar un evento de prueba; el cuerpo {"eventType": "content.completed"} define la forma |
GET /webhooks/{id}/deliveries | las últimas 50 entregas: estado, código de respuesta, latencia, inicio de la respuesta |
POST /webhooks/{id}/deliveries/{deliveryId}/resend | reenviar una entrega; deliveryId es el _id de una fila del registro |
PATCH sin ningún evento válido en events o con un status distinto de active / disabled se rechaza con 400. DELETE de una suscripción desconocida responde 404. En el registro de entregas, payload es el data del evento.
Eventos
Trabajos terminados
| Evento | Cuándo |
|---|---|
valuation.completed, valuation.failed | informe de valoración completo |
review.completed, review.failed | revisión de informe |
content.completed, content.partial, content.failed | paquete de contenido (partial: algunos archivos fallaron) |
text.completed, text.failed | textos |
video.completed, video.failed | vídeo |
avatar.completed, avatar.failed | vídeo con avatar |
photo.completed, photo.failed | mejora de fotos |
floorplan.completed, floorplan.failed | plano |
pdf.completed, pdf.failed | presentación en PDF |
data: { "jobId", "status", "state" }. status es el estado original de la herramienta en mayúsculas (por ejemplo, COMPLETED); decida según state. Si un trabajo no ha terminado en 30 minutos, .failed con "reason": "timeout". Un trabajo se sigue solo si al crearlo ya existía una suscripción a su evento (Trabajos y resultados).
Inmuebles
| Evento | Cuándo |
|---|---|
object.created | ha creado un inmueble |
object.updated | un inmueble ha cambiado, también por un moderador |
object.published | un inmueble se ha hecho público |
object.deleted | se ha eliminado un borrador o archivado un inmueble publicado |
data: { "objectId", "kind", "status", "occurredAt" }; en object.deleted, en lugar de status va mode (deleted o archived). Los datos completos: GET /objects/{kind}/{id}.
Leads
| Evento | Cuándo |
|---|---|
lead.created | un lead nuevo para usted |
lead.updated | ha cambiado el estado o el responsable |
data: { "leadId", "status", "type", "source", "objectId", "occurredAt" }. El evento no contiene datos personales del cliente: el lead se lee con GET /leads/{id}.
Swipe Match
| Evento | Cuándo |
|---|---|
match.lead.offered | se le ha ofrecido un lead (a usted o a un agente de su agencia) |
match.lead.offer_closed | la oferta se ha cerrado sin usted (tiempo agotado) |
match.lead.matched | el lead se le ha asignado |
match.lead.updated | el lead ha pasado a una nueva etapa |
match.lead.unassigned | el lead se le ha retirado |
data: { "leadId", "eventId", "eventCode", "occurredAt", "source", "stage", "active", "leadKind", "clientType", "country", "brokerId", "agencyId" }. El nombre, el teléfono y el correo del cliente nunca van en los eventos (Swipe Match).
Formato de entrega
POST https://example.com/grem-hook
Content-Type: application/json
x-grem-event: content.completed
x-grem-signature: t=1759400000,v1=3b78a03b78058d229023db2a0b4ee072...
{ "id": "...", "type": "content.completed", "createdAt": "2026-10-02T10:00:00.000Z", "data": { ... } }
ides igual en todos los reintentos de una entrega: úselo para no procesar un evento dos veces.tycreatedAtson el momento del intento concreto.- Una entrega de prueba (
POST /webhooks/{id}/test) con uneventTypede la lista llevatest: trueendata; sin él, el tipo de evento eswebhook.test. UneventTypeque no está en la lista es400.webhook.testen sí se acepta.
Verificar la firma
La firma es un HMAC-SHA256 de la cadena "<t>.<cuerpo sin procesar>" con el secreto de la suscripción, en hexadecimal en v1. Verifíquela antes de cualquier procesamiento y rechace los t antiguos (protección contra reenvíos).
Calcule la firma sobre el cuerpo sin procesar, tal como llegó, antes de analizar el JSON. Volver a serializarlo cambia los bytes y la firma no coincidirá.
const crypto = require('crypto');
function verifyGremSignature(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(String(header || '').split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/grem-hook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyGremSignature(process.env.GREM_WEBHOOK_SECRET, req.get('x-grem-signature'), raw)) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(raw);
// event.id, event.type, event.data
res.sendStatus(200);
});
En el SDK lo hace verifyWebhookSignature().
Reintentos y desactivación
- Una entrega es correcta si su servidor responde
2xxen 10 segundos. Responda rápido y haga el trabajo pesado en segundo plano. - Una entrega fallida se reintenta a los 5 minutos, 30 minutos, 2 horas y 6 horas: hasta 5 intentos en total.
- Tras 5 entregas seguidas totalmente fallidas, la suscripción se desactiva (
status: disabled). Reactívela conPATCH /webhooks/{id}y{"status":"active"}o en el panel. - El registro de entregas (
GET /webhooks/{id}/deliveries) muestra el código de respuesta de su servidor y el inicio del cuerpo de la respuesta: así verá por qué falló una entrega.
Artículos relacionados
- Trabajos y resultados: estados de los trabajos.
- Swipe Match: códigos de eventos de los leads del módulo.