Un webhook est un POST que GREM envoie à votre URL lorsque quelque chose se produit : une tâche se termine, une annonce ou un lead change. La gestion des abonnements nécessite le scope webhooks et n'est pas facturée. Les webhooks d'une agence se gèrent avec la clé de l'agence entière ; une clé limitée à un agent reçoit 403.
S'abonner
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- une adresse publiquehttps://sur le port par défaut (443), sansuser:password. Les autres ports, les identifiants dans l'adresse, les adresses privées, locales et internes sont refusés (400). La règle du port s'applique au moment où vous définissez une adresse : les endpoints enregistrés auparavant sur un autre port continuent de recevoir les livraisons.events- au moins un événement de la liste ci-dessous ; les noms inconnus sont ignorés.secret- le secret de signature. Il n'est renvoyé qu'une seule fois - conservez-le.- Jusqu'à 10 adresses par compte, et séparément jusqu'à 10 adresses de bac à sable créées avec des clés de test ; la 11e est refusée avec
422 business_rule. - Les endpoints créés avec une clé de test sont des endpoints de bac à sable : ils ne reçoivent que les événements des tâches du bac à sable (Bac à sable).
Les abonnements peuvent aussi être gérés dans l'espace personnel : API → Webhooks. Le secret de signature peut y être changé.
Gestion
| Requête | Ce qu'elle fait |
|---|---|
GET /webhooks | vos abonnements (sans les secrets) |
PATCH /webhooks/{id} | modifier url, events ou status (active / disabled) |
DELETE /webhooks/{id} | supprimer un abonnement |
POST /webhooks/{id}/test | envoyer un événement d'exemple ; le corps {"eventType": "content.completed"} définit la forme de l'exemple |
GET /webhooks/{id}/deliveries | les 50 dernières livraisons : statut, code de réponse, latence, début de la réponse |
POST /webhooks/{id}/deliveries/{deliveryId}/resend | renvoyer une livraison ; deliveryId est le _id d'une ligne du journal |
Un PATCH sans aucun événement valide dans events ou avec un status autre que active / disabled est refusé avec 400. Un DELETE d'un abonnement inconnu répond 404. Dans le journal des livraisons, payload correspond au data de l'événement.
Événements
Tâches terminées
| Événement | Quand |
|---|---|
valuation.completed, valuation.failed | rapport d'évaluation complet |
express.completed, express.failed | estimation express lancée avec Prefer: respond-async |
rent.completed, rent.failed | estimation du loyer lancée avec Prefer: respond-async |
draft.completed, draft.failed | brouillon d’annonce lancé avec Prefer: respond-async |
site.completed, site.failed | page du bien lancée avec Prefer: respond-async |
review.completed, review.failed | vérification de rapport |
review.action_required | la vérification de rapport a lu le document et attend confirm |
content.completed, content.partial, content.failed | pack de contenu (partial - certains fichiers ont échoué) |
text.completed, text.failed | textes |
video.completed, video.failed | vidéo |
avatar.completed, avatar.failed | vidéo d'avatar |
photo.completed, photo.failed | amélioration de photos |
floorplan.completed, floorplan.failed | plan |
pdf.completed, pdf.failed | présentation PDF |
data : { "jobId", "status", "state" }. status est le statut brut de l'outil en majuscules (par exemple COMPLETED) - basez vos branchements sur state. Si une tâche ne s'est pas terminée en 30 minutes - .failed avec "reason": "timeout". Une tâche n'est suivie que si un abonnement à son événement existait au moment de sa création (Tâches et résultats). Dans les événements express.* et rent.*, status vaut DONE ou FAILED. Pour un plan ou une vérification de rapport lancés avec Prefer: respond-async, l'événement porte l'identifiant de la tâche de requête (areq_…) ; review.completed d'une telle tâche signifie que le document est lu et nécessite confirm - la vérification elle-même envoie son propre événement après confirmation, comme d'habitude.
review.action_required arrive une fois, quand une vérification de rapport a lu le document et attend confirm : jobId est la vérification elle-même (aussi lancée avec Prefer: respond-async), status vaut NEED_CONFIRM, state vaut action_required.
Annonces
| Événement | Quand |
|---|---|
object.created | vous avez créé une annonce |
object.updated | une annonce a changé, y compris par un modérateur |
object.published | une annonce est devenue publique |
object.deleted | un brouillon a été supprimé ou une annonce publiée archivée |
data : { "objectId", "kind", "status", "occurredAt" } ; object.deleted porte mode (deleted ou archived) au lieu de status. Données complètes - GET /objects/{kind}/{id}.
Leads
| Événement | Quand |
|---|---|
lead.created | un nouveau lead qui vous est adressé |
lead.updated | le statut ou le responsable a changé |
data : { "leadId", "status", "type", "source", "objectId", "occurredAt" }. Aucune donnée personnelle du client dans l'événement - lisez le lead complet avec GET /leads/{id}.
Swipe Match
| Événement | Quand |
|---|---|
match.lead.offered | un lead vous a été proposé (ou à un agent de votre agence) |
match.lead.offer_closed | l'offre s'est fermée sans vous (le délai a expiré) |
match.lead.matched | le lead vous a été attribué |
match.lead.updated | le lead est passé à une nouvelle étape |
match.lead.unassigned | le lead vous a été retiré |
data : { "leadId", "eventId", "eventCode", "occurredAt", "source", "stage", "active", "leadKind", "clientType", "country", "brokerId", "agencyId" }. Le nom, le téléphone et l'e-mail du client ne figurent jamais dans les événements (Swipe Match).
Format de livraison
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": { ... } }
idest identique dans toutes les nouvelles tentatives d'une même livraison - utilisez-le pour éviter de traiter un événement deux fois.tetcreatedAtcorrespondent à l'heure de la tentative concernée.- Une livraison de test (
POST /webhooks/{id}/test) avec uneventTypede la liste portetest: truedansdata; sans lui, le type d'événement estwebhook.test. UneventTypeabsent de la liste donne400.webhook.testlui-même est accepté.
Vérifier la signature
La signature est un HMAC-SHA256 de la chaîne "<t>.<raw body>" calculé avec le secret de l'abonnement, encodé en hexadécimal dans v1. Vérifiez-la avant tout traitement et rejetez les valeurs t trop anciennes (protection contre le rejeu).
Calculez la signature sur le corps brut de la requête, exactement tel que reçu, avant de parser le JSON. Une re-sérialisation modifie les octets et la signature ne correspondra pas.
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);
});
Dans le SDK, verifyWebhookSignature() fait la même chose.
Nouvelles tentatives et désactivation
- Une livraison réussit lorsque votre serveur répond
2xxen moins de 10 secondes. Répondez vite et effectuez le travail lourd en arrière-plan. - Une livraison échouée est retentée après 5 minutes, 30 minutes, 2 heures et 6 heures - jusqu'à 5 tentatives au total.
- Après 5 livraisons consécutives totalement échouées, l'abonnement est désactivé (
status: disabled). Réactivez-le avecPATCH /webhooks/{id}et{"status":"active"}, ou dans l'espace personnel. - Le journal des livraisons (
GET /webhooks/{id}/deliveries) affiche le code de réponse de votre serveur et le début de son corps - c'est là que vous voyez pourquoi une livraison a échoué.
Voir aussi
- Tâches et résultats - états des tâches.
- Swipe Match - codes d'événements des leads du module.