scorimmo 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,598 @@
1
+ Metadata-Version: 2.4
2
+ Name: scorimmo
3
+ Version: 0.2.0
4
+ Summary: Official Scorimmo SDK for Python — API v2 client & webhook handler
5
+ License: MIT
6
+ Keywords: scorimmo,real-estate,crm,leads,webhook,sdk,immobilier
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.27.0
10
+ Provides-Extra: flask
11
+ Requires-Dist: flask>=3.0.0; extra == "flask"
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
15
+ Requires-Dist: respx>=0.21.0; extra == "dev"
16
+
17
+ # scorimmo
18
+
19
+ SDK officiel Python pour la plateforme [Scorimmo](https://pro.scorimmo.com) — client API **v2** & récepteur webhook.
20
+
21
+ - **Client API** — récupérez, filtrez et mettez à jour vos leads avec gestion automatique du JWT (access + refresh token, rotation).
22
+ - **Réception de webhooks** — recevez les événements Scorimmo en temps réel, avec vérification optionnelle de la signature HMAC-SHA256.
23
+
24
+ > **Documentation de référence :**
25
+ > [API REST v2](https://pro.scorimmo.com/api/v2/doc) · [Webhooks](https://pro.scorimmo.com/webhook/doc)
26
+
27
+ ---
28
+
29
+ ## Sommaire
30
+
31
+ - [Installation](#installation)
32
+ - [Identifiants API](#identifiants-api)
33
+ - [Client API](#client-api)
34
+ - [Webhooks](#webhooks)
35
+ - [Intégration Flask](#intégration-flask)
36
+ - [Référence — Ressources](#référence--ressources)
37
+ - [Référence — Gestion des tokens](#référence--gestion-des-tokens)
38
+ - [Référence — Événements webhook](#référence--événements-webhook)
39
+ - [Gestion des erreurs](#gestion-des-erreurs)
40
+ - [Support](#support)
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install scorimmo
48
+
49
+ # Avec support Flask (vue webhook clé-en-main)
50
+ pip install scorimmo[flask]
51
+ ```
52
+
53
+ **Prérequis :** Python ≥ 3.9
54
+
55
+ ---
56
+
57
+ ## Identifiants API
58
+
59
+ Les identifiants (`email` / `password`) sont ceux fournis par Scorimmo. L'identifiant est l'adresse email du compte API v2.
60
+
61
+ Après le premier appel authentifié, un **refresh token** est disponible via `get_refresh_token()`. Il permet de réinitialiser le client sans repasser par les identifiants (voir [Gestion des tokens](#référence--gestion-des-tokens)).
62
+
63
+ Pour le webhook, le secret HMAC (`SCORIMMO_WEBHOOK_SIGNATURE_SECRET`) est une valeur que vous choisissez librement — communiquez-la ensuite à Scorimmo lors de la configuration (voir [Configurer le webhook chez Scorimmo](#configurer-le-webhook-chez-scorimmo)).
64
+
65
+ ---
66
+
67
+ ## Client API
68
+
69
+ ### Initialisation
70
+
71
+ **Avec identifiants email/password :**
72
+
73
+ ```python
74
+ from scorimmo import ScorimmoClient
75
+
76
+ client = ScorimmoClient(
77
+ email="votre-email",
78
+ password="votre-mot-de-passe",
79
+ # base_url="https://pro.scorimmo.com" # par défaut
80
+ )
81
+ ```
82
+
83
+ **Avec un refresh token persisté (sans exposer les identifiants) :**
84
+
85
+ ```python
86
+ client = ScorimmoClient(refresh_token=persisted_token)
87
+ ```
88
+
89
+ Le token JWT est géré automatiquement : refresh silencieux à l'expiration puis fallback sur email/password si nécessaire.
90
+
91
+ ### Récupérer les leads récents
92
+
93
+ ```python
94
+ from datetime import datetime, timedelta, timezone
95
+
96
+ # Tous les leads des dernières 24 heures — pagination + dédup automatiques
97
+ since = datetime.now(timezone.utc) - timedelta(hours=24)
98
+ leads = client.leads.since(since, include=["customer", "seller"])
99
+
100
+ # Depuis une date précise
101
+ leads = client.leads.since("2026-06-01 00:00:00")
102
+
103
+ # Leads modifiés récemment (plutôt que créés)
104
+ leads = client.leads.since(since, field="updated_at")
105
+
106
+ # Restreindre à un point de vente + callback de progression
107
+ leads = client.leads.since(
108
+ since,
109
+ store_id=776,
110
+ include=["customer", "seller"],
111
+ on_progress=lambda page, count, total, meta: print(f"Page {page}: {total} leads"),
112
+ )
113
+ ```
114
+
115
+ ### Récupérer un lead par ID
116
+
117
+ ```python
118
+ lead = client.leads.get(42)
119
+ lead = client.leads.get(42, include=["customer", "seller", "appointments", "comments"])
120
+ ```
121
+
122
+ ### Rechercher des leads
123
+
124
+ ```python
125
+ result = client.leads.list(
126
+ interest="Transaction",
127
+ store_id=1,
128
+ **{"created_at[gte]": "2026-01-01T00:00:00+00:00"},
129
+ sort="created_at:desc",
130
+ limit=20,
131
+ )
132
+
133
+ # result["data"] contient les leads, result["meta"] contient la pagination
134
+ for lead in result["data"]:
135
+ print(lead["id"], lead.get("customer_id"))
136
+ ```
137
+
138
+ Filtres complets disponibles :
139
+
140
+ | Paramètre | Type | Description |
141
+ |---|---|---|
142
+ | `page` | `int` | Numéro de page (défaut : 1) |
143
+ | `limit` | `int` | Résultats par page (défaut : 10, max : 100) |
144
+ | `sort` | `str` | `"champ:asc"` ou `"champ:desc"` — champs : `id`, `created_at`, `updated_at`, `status` |
145
+ | `include` | `str` | Relations : `"customer,seller,appointments,reminders,requests,comments"` |
146
+ | `store_id` | `int` | Restreindre à un point de vente |
147
+ | `seller_id` | `int` | Restreindre à un conseiller |
148
+ | `status` | `str` | Statut du lead |
149
+ | `substatus` | `str` | Sous-statut |
150
+ | `interest` | `str` | `TRANSACTION`, `LOCATION`, `GESTION`… |
151
+ | `origin` | `str` | Origine du lead |
152
+ | `contact_type` | `str` | `physical`, `phone` ou `digital` |
153
+ | `purpose` | `str` | Achat, Location, Bailleur, Vente, Recherche, Locataire, Non renseigné |
154
+ | `customer_first_name` | `str` | Prénom du contact |
155
+ | `customer_last_name` | `str` | Nom du contact |
156
+ | `customer.email` | `str` | Email (à passer via `**{"customer.email": …}`) |
157
+ | `customer.phone` | `str` | Téléphone (OR sur phone/other_phone) |
158
+ | `external_lead_id` | `str` | Référence CRM |
159
+ | `requests_reference` | `str` | Référence du bien |
160
+ | `ids` | `str` | IDs multiples séparés par virgule |
161
+ | `created_at[gte\|lte\|eq]` | `str` | Filtres de date ISO 8601 |
162
+ | `updated_at[gte\|lte\|eq]` | `str` | Filtres de date ISO 8601 |
163
+
164
+ ### Mise à jour partielle
165
+
166
+ ```python
167
+ client.leads.update(42, {"external_lead_id": "CRM-456", "seller_id": 3533})
168
+ ```
169
+
170
+ > **Création de leads :** volontairement non exposée par le SDK. Les leads doivent être créés via l'interface Scorimmo, un formulaire (`client.form.submit`), un webcallback ou un import.
171
+
172
+ ---
173
+
174
+ ## Webhooks
175
+
176
+ ### Initialisation
177
+
178
+ L'authentification des webhooks est **optionnelle** — la sécurisation de l'endpoint est à la charge de l'intégrateur. Scorimmo propose plusieurs mécanismes complémentaires :
179
+
180
+ 1. **Signature HMAC-SHA256** (fortement recommandé) — Scorimmo signe le corps brut avec un secret partagé et envoie la signature dans le header `X-Signature-256` sous la forme `sha256=<hex>`. Le SDK la vérifie en temps constant via `hmac.compare_digest()`.
181
+ 2. **HTTP Basic auth via URL** — enregistrez le webhook comme `https://user:pass@host/path`, l'auth est déléguée au serveur/framework, transparente pour le SDK.
182
+ 3. **Restriction réseau** (IP whitelist, VPN, mTLS…) — hors périmètre du SDK.
183
+
184
+ **Avec signature HMAC (recommandé) :**
185
+
186
+ ```python
187
+ import os
188
+ from scorimmo import ScorimmoWebhook
189
+
190
+ webhook = ScorimmoWebhook(
191
+ signature_secret=os.environ["SCORIMMO_WEBHOOK_SIGNATURE_SECRET"],
192
+ # signature_header="X-Signature-256", # valeur par défaut
193
+ )
194
+ ```
195
+
196
+ **Sans vérification** (auth déportée sur le serveur) :
197
+
198
+ ```python
199
+ webhook = ScorimmoWebhook()
200
+ ```
201
+
202
+ > **Important :** si vous activez la signature, le corps brut doit être passé sous forme de `bytes` (ou `str`) **avant tout parsing JSON** — sinon la signature ne pourra pas être vérifiée. En Flask : `request.get_data()` ; en FastAPI : `await request.body()`.
203
+
204
+ ### Traitement d'une requête entrante (générique)
205
+
206
+ ```python
207
+ from scorimmo import WebhookAuthError, WebhookValidationError
208
+
209
+ # headers : dict des en-têtes HTTP (str ou list, insensible à la casse)
210
+ # raw_body : corps brut de la requête (bytes recommandé)
211
+ try:
212
+ webhook.handle(headers, raw_body, {
213
+ "new_lead": on_new_lead,
214
+ "update_lead": on_update_lead,
215
+ "new_comment": on_new_comment,
216
+ "new_rdv": on_new_rdv,
217
+ "new_reminder": on_new_reminder,
218
+ "closure_lead": on_closure_lead,
219
+ # Événement futur inconnu (arrivé avec X-Scorimmo-Event: webhook.<name>) :
220
+ "unknown": lambda event: print(f"Événement inconnu : {event}"),
221
+ })
222
+ # → HTTP 200
223
+ except WebhookAuthError:
224
+ # Signature manquante ou invalide → HTTP 401
225
+ pass
226
+ except WebhookValidationError:
227
+ # Payload JSON invalide ou champ "event" manquant → HTTP 400
228
+ pass
229
+ ```
230
+
231
+ ### Headers webhook v2
232
+
233
+ Chaque requête webhook envoyée par Scorimmo inclut ces headers :
234
+
235
+ | Header | Exemple | Description |
236
+ |---|---|---|
237
+ | `X-Signature-256` | `sha256=8f4c…` | Signature HMAC-SHA256 du corps brut (présent si vous avez configuré un secret ; nom personnalisable) |
238
+ | `X-Scorimmo-Event` | `lead.created` | Nom sémantique de l'événement |
239
+ | `X-Scorimmo-Version` | `2026-04-20` | Version d'API (date, format `YYYY-MM-DD` — pas un numéro sémantique) |
240
+ | `User-Agent` | `Scorimmo/1.42.0` | Version applicative Scorimmo (distincte de `X-Scorimmo-Version`) |
241
+
242
+ ```python
243
+ event_name = webhook.get_semantic_event(headers) # ex: 'lead.created'
244
+ api_version = webhook.get_api_version(headers) # ex: '2026-04-20'
245
+ ```
246
+
247
+ Correspondance entre le champ `event` du payload et `X-Scorimmo-Event` :
248
+
249
+ | `event` (payload) | `X-Scorimmo-Event` |
250
+ |---|---|
251
+ | `new_lead` | `lead.created` |
252
+ | `update_lead` | `lead.updated` |
253
+ | `closure_lead` | `lead.closed` |
254
+ | `new_comment` | `lead.comment_added` |
255
+ | `new_rdv` | `lead.appointment_created` |
256
+ | `new_reminder` | `lead.reminder_created` |
257
+ | _(événement futur inconnu)_ | `webhook.<name>` |
258
+
259
+ > Enregistrez un handler `'unknown'` pour capturer les événements futurs non encore modélisés — Scorimmo peut ajouter de nouveaux événements sans breaking change.
260
+
261
+ ### Idempotence & retries
262
+
263
+ Scorimmo effectue jusqu'à **6 tentatives de livraison** (initial + 5 retries) avec backoff exponentiel (5 s → 60 s max). Le corps et la signature sont identiques à chaque retry.
264
+
265
+ Aucun `Idempotency-Key` n'est envoyé — votre receveur doit être idempotent, typiquement en dédupliquant sur `(event, lead_id, created_at)` ou `(event, id, updated_at)` selon le type d'événement.
266
+
267
+ ### Configurer le webhook chez Scorimmo
268
+
269
+ Une fois votre endpoint déployé, transmettez les informations suivantes à votre **account manager Scorimmo** (voir [Support](#support)) :
270
+
271
+ ```
272
+ URL du webhook : https://votre-app.com/webhook/scorimmo
273
+
274
+ Authentification (au choix, fortement recommandé) :
275
+ Option A - Signature HMAC-SHA256 (recommandé)
276
+ Le back-office Scorimmo n'a pas de champ « secret » dédié : la signature
277
+ se configure via un header custom, en convention in-place.
278
+
279
+ Dans l'écran de configuration du webhook, ajoutez un header custom :
280
+ Nom : X-Signature-256
281
+ (nom libre côté back, MAIS il doit correspondre à
282
+ `signature_header` côté SDK — valeur par défaut du SDK :
283
+ X-Signature-256. Si vous choisissez un autre nom, pensez à
284
+ le passer via `signature_header=` à `ScorimmoWebhook`.)
285
+ Valeur : sha256=<votre-secret>
286
+ (le préfixe `sha256=` est obligatoire — c'est ce qui déclenche
287
+ la convention. Le back détecte le préfixe, extrait la partie
288
+ après `sha256=` comme secret HMAC, puis remplace la valeur
289
+ envoyée par :
290
+ sha256=<hex(hmac_sha256(rawBody, <votre-secret>))>
291
+ avant l'appel HTTP.)
292
+
293
+ Secret côté SDK :
294
+ La chaîne que vous avez collée après `sha256=` ci-dessus doit être
295
+ strictement identique à `SCORIMMO_WEBHOOK_SIGNATURE_SECRET`
296
+ (paramètre `signature_secret` de `ScorimmoWebhook`).
297
+
298
+ Option B - HTTP Basic auth via URL
299
+ URL : https://user:pass@votre-app.com/webhook/scorimmo
300
+
301
+ Événements à activer :
302
+ ☑ Nouveau lead (new_lead)
303
+ ☑ Mise à jour lead (update_lead)
304
+ ☑ Nouveau commentaire (new_comment)
305
+ ☑ Rendez-vous (new_rdv)
306
+ ☑ Rappel (new_reminder)
307
+ ☑ Clôture lead (closure_lead)
308
+
309
+ Point(s) de vente concerné(s) : [indiquez vos points de vente]
310
+ ```
311
+
312
+ > **Important :** Scorimmo considère la livraison réussie uniquement si votre endpoint retourne HTTP 200.
313
+
314
+ ---
315
+
316
+ ## Intégration Flask
317
+
318
+ ```bash
319
+ pip install scorimmo[flask]
320
+ ```
321
+
322
+ ```python
323
+ import os
324
+ from flask import Flask
325
+ from scorimmo import ScorimmoWebhook
326
+
327
+ app = Flask(__name__)
328
+
329
+ webhook = ScorimmoWebhook(
330
+ signature_secret=os.environ.get("SCORIMMO_WEBHOOK_SIGNATURE_SECRET"),
331
+ )
332
+
333
+ app.add_url_rule(
334
+ "/webhook/scorimmo",
335
+ view_func=webhook.flask_view({
336
+ "new_lead": lambda event: on_new_lead(event),
337
+ "update_lead": lambda event: on_update_lead(event),
338
+ "new_comment": lambda event: on_new_comment(event),
339
+ "new_rdv": lambda event: on_new_rdv(event),
340
+ "new_reminder": lambda event: on_new_reminder(event),
341
+ "closure_lead": lambda event: on_closure_lead(event),
342
+ "unknown": lambda event: log(event),
343
+ }),
344
+ methods=["POST"],
345
+ )
346
+ ```
347
+
348
+ La vue générée par `flask_view()` :
349
+ - lit le corps brut via `request.get_data()` avant tout `json_decode()`,
350
+ - retourne `401` si la signature HMAC est absente ou invalide,
351
+ - retourne `400` si le payload est mal formé,
352
+ - retourne `{"ok": true}` avec HTTP 200 en cas de succès.
353
+
354
+ ---
355
+
356
+ ## Référence — Ressources
357
+
358
+ Toutes les ressources ci-dessous exposent `list(**query)` (et `get(id)` quand l'endpoint le permet).
359
+
360
+ ### Leads — `client.leads`
361
+
362
+ Voir [Client API](#client-api) ci-dessus. Méthodes : `get`, `list`, `update`, `since`.
363
+
364
+ ### Rendez-vous — `client.appointments`
365
+
366
+ | Paramètre | Type | Description |
367
+ |---|---|---|
368
+ | `lead_id` | `int` | Filtrer par lead |
369
+ | `ids` | `str` | IDs séparés par virgule |
370
+ | `created_at[gte\|lte\|eq]` | `str` | Filtres de date (ISO 8601) |
371
+ | `updated_at[gte\|lte]` | `str` | Idem |
372
+ | `start_time[gte\|lte\|eq]` | `str` | Idem |
373
+ | `sort` | `str` | `id`, `created_at`, `updated_at`, `start_time` (avec `:asc`/`:desc`) |
374
+
375
+ ### Commentaires — `client.comments`
376
+
377
+ | Paramètre | Type | Description |
378
+ |---|---|---|
379
+ | `lead_id` | `int` | Filtrer par lead |
380
+ | `ids` | `str` | IDs séparés par virgule |
381
+ | `created_at[gte\|lte\|eq]` | `str` | Filtres de date |
382
+ | `sort` | `str` | `id`, `created_at` |
383
+
384
+ ### Rappels — `client.reminders`
385
+
386
+ | Paramètre | Type | Description |
387
+ |---|---|---|
388
+ | `lead_id` | `int` | Filtrer par lead |
389
+ | `ids` | `str` | IDs séparés par virgule |
390
+ | `created_at[gte\|lte\|eq]` | `str` | Filtres de date |
391
+ | `updated_at[gte\|lte]` | `str` | Idem |
392
+ | `start_time[gte\|lte\|eq]` | `str` | Idem (`reminder_date` côté serveur) |
393
+ | `sort` | `str` | `id`, `created_at`, `updated_at`, `start_time` |
394
+
395
+ ### Demandes — `client.requests`
396
+
397
+ | Paramètre | Type | Description |
398
+ |---|---|---|
399
+ | `lead_id` | `int` | Filtrer par lead |
400
+ | `reference` | `str` | Référence du bien |
401
+ | `ids` | `str` | IDs séparés par virgule |
402
+ | `created_at[gte\|lte\|eq]` | `str` | Filtres de date |
403
+ | `updated_at[gte\|lte]` | `str` | Idem |
404
+ | `sort` | `str` | `id`, `created_at`, `updated_at` |
405
+
406
+ ### Contacts — `client.customers`
407
+
408
+ | Paramètre | Type | Description |
409
+ |---|---|---|
410
+ | `search` | `str` | Recherche full-text |
411
+ | `email` | `str` | Recherche par email |
412
+ | `phone` | `str` | Recherche par téléphone (OR sur phone/other_phone) |
413
+ | `sort` | `str` | `id` |
414
+
415
+ ### Origines — `client.origins`
416
+
417
+ | Paramètre | Type | Description |
418
+ |---|---|---|
419
+ | `store_id` | `int` | Filtrer par point de vente |
420
+ | `has_tracking` | `bool` | `True` = origines avec au moins un traceur actif |
421
+ | `tracking_channel` | `str` | `phone` ou `email` (validé, sinon `ValueError`) |
422
+ | `include` | `str` | `tracking` pour inclure les numéros/emails traceurs |
423
+
424
+ ### Utilisateurs — `client.users`
425
+
426
+ | Paramètre | Type | Description |
427
+ |---|---|---|
428
+ | `store_id` | `int` | Filtrer par point de vente |
429
+ | `interest` | `str` | Filtrer par intérêt |
430
+ | `role` | `str` | `admin`, `manager`, `agent` ou `virtual` (validé, sinon `ValueError`) |
431
+ | `sort` | `str` | `id`, `last_name`, `created_at` |
432
+
433
+ ### Statuts — `client.status`
434
+
435
+ | Paramètre | Type | Description |
436
+ |---|---|---|
437
+ | `ids` | `str` | Liste d'ids séparés par virgule |
438
+ | `interest` | `str \| list` | Liste CSV d'intérêts (`"TRANSACTION,LOCATION"`) ou liste Python |
439
+ | `store_id` | `str \| list` | Idem pour les points de vente |
440
+
441
+ Réponse : `[{"label": "...", "sub_status": [...] | null}, …]`.
442
+
443
+ ### Points de vente / Champs additionnels / Champs de demande
444
+
445
+ - `client.stores` — GET `/api/v2/stores`, GET `/api/v2/stores/{id}`
446
+ - `client.additional_fields` — GET `/api/v2/additional_fields` (`store_id`, `interest`)
447
+ - `client.request_fields` — GET `/api/v2/requests/fields` (`store_id`, `interest`)
448
+
449
+ ### Formulaires publics — `client.form`
450
+
451
+ Soumission d'un formulaire de contact qui crée un lead et envoie un email au destinataire. **Scope requis : `ROLE_API_FORM_WRITE`** (à demander séparément de `lead:write`).
452
+
453
+ ```python
454
+ response = client.form.submit({
455
+ "store_id": 776,
456
+ "libelle_id": 12,
457
+ "to_email": "contact@agence.fr", # ou list de destinataires
458
+ "origin": "Site web",
459
+ "message": "Je souhaite visiter le bien X.",
460
+ "subject": "Demande de visite", # optionnel
461
+ "customer": {
462
+ "civility": "M.",
463
+ "first_name": "Jean",
464
+ "last_name": "Dupont",
465
+ "email": "jean@example.com",
466
+ "phone": "0612345678",
467
+ },
468
+ "requests": [{"...": "..."}], # optionnel, labels du référentiel
469
+ "additional_fields": [{"...": "..."}], # optionnel, labels du référentiel
470
+ "external_lead_id": "CRM-12345", # optionnel
471
+ })
472
+ # response == {"status": 200, "message": "email created", "id": 42, "store_id": 776, ...}
473
+ ```
474
+
475
+ ### Appels sortants — `client.web_callbacks`
476
+
477
+ Déclenche un appel depuis le PBX Scorimmo vers un numéro. **N'utilise pas** l'authentification Bearer : passez la clé personnelle `WebCallback` fournie par Scorimmo pour votre point de vente.
478
+
479
+ ```python
480
+ client.web_callbacks.launch("votre-cle-wcb", "+33612345678")
481
+ # {"results": ["..."], "information": 200}
482
+ ```
483
+
484
+ ---
485
+
486
+ ## Référence — Gestion des tokens
487
+
488
+ Le client gère automatiquement l'access token. À chaque expiration, il tente d'abord un refresh silencieux, puis bascule sur email/password si nécessaire.
489
+
490
+ ```python
491
+ # 1. Premier démarrage — authentification par identifiants
492
+ client = ScorimmoClient(email="...", password="...")
493
+
494
+ # 2. Forcer l'auth initiale et récupérer le refresh token
495
+ client.get_token()
496
+ refresh_token = client.get_refresh_token()
497
+ # → persister refresh_token (cache Redis, base de données, coffre-fort…)
498
+
499
+ # 3. Démarrages suivants — sans identifiants
500
+ refresh_token = ... # charger depuis le stockage
501
+ client = ScorimmoClient(refresh_token=refresh_token)
502
+
503
+ # 4. Après chaque session, le refresh token a tourné — le re-persister
504
+ client.get_token()
505
+ new_refresh_token = client.get_refresh_token()
506
+ # → mettre à jour le stockage
507
+ ```
508
+
509
+ > **Rotation automatique :** chaque refresh token ne peut être utilisé qu'une seule fois. Le nouveau refresh token est disponible via `get_refresh_token()` après chaque renouvellement.
510
+
511
+ Méthodes disponibles :
512
+
513
+ - `client.get_token() -> str`
514
+ - `client.get_refresh_token() -> str | None`
515
+ - `client.refresh_access_token(refresh_token) -> dict`
516
+ - `client.revoke_token(refresh_token=None) -> dict` — `None` révoque tous les tokens du compte
517
+ - `client.validate_token() -> dict` — retourne `version`, `status`, `authenticated`, `scopes`, `stores_id`, `interests`
518
+
519
+ ---
520
+
521
+ ## Référence — Événements webhook
522
+
523
+ ### `new_lead` — Nouveau lead reçu
524
+
525
+ Payload : objet lead complet — `id`, `store_id`, `interest`, `status`, `origin`, `contact_type`, `seller_present_on_creation`, `customer` (`first_name`, `last_name`, `email`, `phone`, `other_phone`, `pro`, `legal_name`, `former`…), `seller` (`id`, `first_name`, `last_name`, `email`, `is_virtual?`), `requests` (liste de biens avec clés en français : `"Type de bien"`, `"Prix"`, `"Surface"`, `"Ville"`, `"Code postal"`, `"Référence"`/`"Programme"`), `additional_fields`, `comments`, `external_lead_id?`, `external_customer_id?`.
526
+
527
+ ### `update_lead` — Lead modifié
528
+
529
+ Payload **sparse** : `id`, `updated_at`, et uniquement les champs modifiés (même forme que ci-dessus mais partiel).
530
+
531
+ ### `new_comment`
532
+
533
+ Payload : `lead_id`, `comment`, `created_at`, `external_lead_id?`.
534
+
535
+ ### `new_rdv`
536
+
537
+ Payload : `lead_id`, `start_time`, `location`, `detail` (nullable — `Estimation`, `Découverte`, `Visite`, `Suivi`, `Proposition`, `Signature`), `comment`, `created_at`, `external_lead_id?`.
538
+
539
+ ### `new_reminder`
540
+
541
+ Payload : `lead_id`, `start_time`, `detail` (`offer` ou `recontact`), `comment`, `created_at`, `external_lead_id?`.
542
+
543
+ ### `closure_lead` — Lead clôturé
544
+
545
+ | Champ | Type | Description |
546
+ |---|---|---|
547
+ | `lead_id` | `int` | Identifiant du lead clôturé |
548
+ | `status` | `str` | Libellé du statut de clôture : `Succès` (vente/location conclue), `Fermé` (abandonné), `Fermé par l'opérateur` |
549
+ | `close_reason` | `str \| None` | Motif de clôture (présent quand `status` = `Fermé` ou `Succès`) |
550
+ | `external_lead_id` | `str \| None` | Référence CRM du lead, si renseignée |
551
+
552
+ ```python
553
+ def on_closure_lead(event: dict) -> None:
554
+ if event["status"] == "Succès":
555
+ # Vente ou location conclue
556
+ ...
557
+ ```
558
+
559
+ > Pour la structure complète de chaque payload, consultez la [documentation webhooks](https://pro.scorimmo.com/webhook/doc).
560
+
561
+ ---
562
+
563
+ ## Gestion des erreurs
564
+
565
+ ```python
566
+ from scorimmo import (
567
+ ScorimmoApiError, ScorimmoAuthError,
568
+ WebhookAuthError, WebhookValidationError,
569
+ )
570
+
571
+ # Erreurs API
572
+ try:
573
+ lead = client.leads.get(999)
574
+ except ScorimmoAuthError:
575
+ # Identifiants incorrects, refresh token révoqué, ou 401 sur endpoint non authentifié
576
+ print("Erreur d'authentification")
577
+ except ScorimmoApiError as e:
578
+ print(f"Erreur API {e.status_code} ({e.api_code}) : {e}")
579
+ # Codes courants : 400 (VALIDATION_ERROR), 403 (FORBIDDEN), 404 (NOT_FOUND)
580
+
581
+ # Erreurs webhook
582
+ try:
583
+ event = webhook.parse(headers, raw_body)
584
+ except WebhookAuthError:
585
+ # Signature manquante ou invalide → HTTP 401
586
+ pass
587
+ except WebhookValidationError:
588
+ # JSON invalide ou champ "event" manquant → HTTP 400
589
+ pass
590
+ ```
591
+
592
+ ---
593
+
594
+ ## Support
595
+
596
+ - Votre account manager Scorimmo
597
+ - [Formulaire de contact](https://pro.scorimmo.com/contact)
598
+ - [pro.scorimmo.com](https://pro.scorimmo.com)