Référence API
L’API partenaire permet à un CRM tiers de lancer des consultations juridiques de 20 minutes au nom de ses clients. Délégation OAuth2, facturation par portefeuille, suivi par webhooks signés.
URL de base
https://avocat20min.be
Version
v1 · OpenAPI 3.1
Format
application/json
Authentification
OAuth2 · Bearer
Modèle de délégation
Authentification
Tous les appels /api/v1 exigent un access token Bearer obtenu via OAuth2. L’access token est un JWT de courte durée (15 min) ; le refresh token est rotatif.
curl -X POST https://avocat20min.be/api/oauth/token \
-d grant_type=authorization_code \
-d code=$CODE \
-d redirect_uri=$REDIRECT_URI \
-d client_id=$AV20_CLIENT_ID \
-d client_secret=$AV20_CLIENT_SECRET \
-d code_verifier=$CODE_VERIFIERSécurité des jetons
Erreurs
Les erreurs renvoient un statut HTTP approprié et un corps JSON { code, message }. Les erreurs de validation incluent un champ issues détaillé.
{
"code": "INSUFFICIENT_FUNDS",
"message": "Solde du portefeuille insuffisant pour lancer la consultation."
}400Requête malformée (en-tête ou paramètre manquant).401Access token absent, invalide ou expiré.403Scope insuffisant pour cette opération.404Ressource introuvable ou hors de votre périmètre.409Conflit métier (solde insuffisant, clé d’idempotence, etc.).422Corps de requête invalide — voir issues.429Quota dépassé — voir l’en-tête Retry-After.
Idempotence
Les requêtes POST de création acceptent — et POST /v1/consultations exige — un en-tête Idempotency-Key. Un rejeu portant la même clé renvoie la réponse mémorisée, sans réexécuter l’opération.
Idempotency-Key: 1f8e2c4a-9b07-4d3e-a210-7c6b5a4e3d2f409 IDEMPOTENCY_CONFLICT. La rétention est de 24 h.Limites de débit
Les appels sont limités par application et par client. Au dépassement, l’API renvoie 429 avec un en-tête Retry-After et des en-têtes X-RateLimit-*.
X-RateLimit-Remaining: 0
Retry-After: 30Obtenir / rafraîchir des jetons
/api/oauth/tokenÉchange un code d’autorisation contre des jetons (grant_type=authorization_code) ou renouvelle l’access token (grant_type=refresh_token). Corps en application/x-www-form-urlencoded.
Corps de la requête
grant_typestringRequisauthorization_coderefresh_tokenType de flux.
codestringOptionnelCode d’autorisation (grant authorization_code).
redirect_uristringOptionnelredirect_uri utilisée à l’étape authorize.
code_verifierstringOptionnelVerifier PKCE (grant authorization_code).
refresh_tokenstringOptionnelRefresh token (grant refresh_token).
client_idstringRequisIdentifiant public de l’application.
client_secretstringRequisSecret de l’application (applications confidentielles).
Réponses
- 200Jetons émis.
- 400invalid_grant / invalid_request.
- 401invalid_client — identifiants d’application invalides.
curl -X POST https://avocat20min.be/api/oauth/token \
-d grant_type=authorization_code \
-d code=$CODE \
-d redirect_uri=$REDIRECT_URI \
-d client_id=$AV20_CLIENT_ID \
-d client_secret=$AV20_CLIENT_SECRET \
-d code_verifier=$CODE_VERIFIERRévoquer un jeton
/api/oauth/revokeRévoque un refresh token (RFC 7009). Renvoie 200 que le jeton ait existé ou non.
Corps de la requête
tokenstringRequisLe refresh token à révoquer.
client_idstringRequisIdentifiant public de l’application.
client_secretstringOptionnelSecret de l’application.
Réponses
- 200Révocation effectuée.
Profil du client
/api/v1/meScope profile:readRenvoie le profil du client connecté. phoneVerified doit être vrai pour pouvoir lancer une consultation.
Réponses
- 200Profil renvoyé.
- 401Jeton absent ou invalide.
{
"id": "usr_9a2b...",
"name": "Marie Lambert",
"email": "marie@example.be",
"phoneVerified": true
}Solde du portefeuille
/api/v1/walletScope wallet:readRenvoie le solde du portefeuille. availableCents = balanceCents − réservations actives ; une consultation coûte 2000 cents.
Réponses
- 200Solde renvoyé.
{
"balanceCents": 6000,
"availableCents": 4000,
"heldCents": 2000,
"currency": "EUR"
}Historique du portefeuille
/api/v1/wallet/transactionsScope wallet:readListe paginée des mouvements du portefeuille, du plus récent au plus ancien.
Paramètres de requête
cursorstringOptionnelCurseur de pagination (nextCursor de la page précédente).
limitintegerOptionneldéfaut20Taille de page (1–50).
Réponses
- 200Liste paginée { items, nextCursor }.
Créer une recharge
/api/v1/wallet/topup-sessionScope wallet:topupCrée une session Stripe Checkout hébergée. Ouvrez checkoutUrl dans un navigateur pour permettre au client de recharger son portefeuille.
Corps de la requête
amountCentsintegerRequisMontant à créditer, en cents (≥ 2000).
returnUrlstringRequisURL de retour après le paiement.
Réponses
- 200Session créée { checkoutUrl, expiresAt }.
- 422Montant ou URL invalides.
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_...",
"expiresAt": "2026-05-20T15:30:00.000Z"
}Domaines de droit
/api/v1/catalog/competencesListe des domaines de droit acceptés par le champ competence d’une consultation.
Réponses
- 200Liste { competences: [{ code, label }] }.
Langues
/api/v1/catalog/languagesListe des langues de consultation acceptées par le champ language.
Réponses
- 200Liste { languages: [{ code, label }] }.
Disponibilité des avocats
/api/v1/coverageScope consultations:readIndique si des avocats sont joignables maintenant pour un couple compétence / langue. À appeler avant de lancer une consultation pour fixer les attentes du client.
Paramètres de requête
competencestringRequisDomaine de droit.
languagestringRequisLangue souhaitée.
Réponses
- 200{ available, lawyersOnline, estimatedWaitSeconds }.
{
"available": true,
"lawyersOnline": 3,
"estimatedWaitSeconds": null
}Lancer une consultation
/api/v1/consultationsScope consultations:writeCrée une consultation, réserve 20 € sur le portefeuille du client et déclenche la recherche d’avocat. Le client est ensuite rappelé sur son téléphone vérifié. En-tête Idempotency-Key obligatoire.
Corps de la requête
competencestringRequisDomaine de droit (voir le catalogue).
languagestringRequisdéfautFRLangue de la consultation.
subjectstringRequisObjet court de la consultation (3 à 200 caractères).
descriptionstringOptionnelDescription détaillée (≤ 2000 caractères).
recordingConsentbooleanOptionneldéfautfalseConsentement du client à l’enregistrement de l’appel.
withdrawalWaiverAcknowledgedbooleanRequisLe client reconnaît le démarrage immédiat de la prestation et renonce à son droit de rétractation. À recueillir auprès du client.
externalRefstringOptionnelRéférence interne de votre CRM, renvoyée dans les webhooks (≤ 100 caractères).
Réponses
- 201Consultation créée, recherche d’avocat en cours.
- 403Scope consultations:write manquant.
- 409INSUFFICIENT_FUNDS, PHONE_REQUIRED ou IDEMPOTENCY_CONFLICT.
- 422Corps invalide ou renonciation non recueillie.
curl -X POST https://avocat20min.be/api/v1/consultations \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: 1f8e...a90" \
-H "Content-Type: application/json" \
-d '{
"competence": "LABOR_LAW",
"language": "FR",
"subject": "Rupture de contrat — calcul du préavis",
"recordingConsent": false,
"withdrawalWaiverAcknowledged": true,
"externalRef": "TICKET-4821"
}'Lister les consultations
/api/v1/consultationsScope consultations:readListe paginée des consultations lancées par votre application pour le client connecté.
Paramètres de requête
statusstringOptionnelMATCHINGASSIGNEDCOMPLETEDCANCELLEDFiltre par statut.
cursorstringOptionnelCurseur de pagination.
limitintegerOptionneldéfaut20Taille de page (1–50).
Réponses
- 200Liste paginée { items, nextCursor }.
Détail d’une consultation
/api/v1/consultations/{id}Scope consultations:readRenvoie une consultation lancée par votre application.
Paramètres de chemin
idstringRequisIdentifiant de la consultation.
Réponses
- 200Consultation renvoyée.
- 404Introuvable ou hors de votre périmètre.
Annuler une consultation
/api/v1/consultations/{id}/cancelScope consultations:writeAnnule une consultation tant que l’appel n’a pas démarré et libère les fonds réservés sur le portefeuille.
Paramètres de chemin
idstringRequisIdentifiant de la consultation.
Réponses
- 200Consultation annulée.
- 409NOT_CANCELLABLE — l’appel a déjà démarré.
Aperçu de la note
/api/v1/consultations/{id}/noteScope notes:readRenvoie l’aperçu de la note rédigée par l’avocat. Le contenu intégral, couvert par le secret professionnel, n’est jamais exposé via l’API.
Paramètres de chemin
idstringRequisIdentifiant de la consultation.
Réponses
- 200{ consultationId, preview, submittedAt }.
- 404Consultation introuvable.
Webhooks — événements
Chaque transition de statut déclenche un POST JSON vers l’URL webhook de votre application. Répondez 2xx sous 5 secondes ; en cas d’échec, la livraison est réessayée avec un backoff exponentiel.
consultation.createdconsultation créée (DRAFT)consultation.payment_authorizedportefeuille débité, recherche d’avocat lancéeconsultation.matchingrecherche d’un avocat en coursconsultation.assignedun avocat a pris la consultationconsultation.call_startedl’appel téléphonique a démarréconsultation.call_endedl’appel s’est terminéconsultation.completedconsultation terminée, note disponibleconsultation.note_availablela note de consultation est disponibleconsultation.pending_callbackaucun avocat sous 30 min — rappel garanti sous 24 hconsultation.cancelledconsultation annuléeconsultation.refundedportefeuille recréditéconsultation.expiredconsultation expirée
{
"id": "evt_3kf9...b21",
"type": "consultation.completed",
"createdAt": "2026-05-20T14:55:12.000Z",
"data": {
"id": "cls7x2a9b0001",
"status": "COMPLETED",
"competence": "LABOR_LAW",
"externalRef": "TICKET-4821",
"durationSec": 1180
}
}Webhooks — signature & vérification
Chaque livraison porte les en-têtes X-Avocat20min-Event, X-Avocat20min-Delivery (identifiant unique, pour l’idempotence) et X-Avocat20min-Signature. Vérifiez la signature HMAC-SHA256 et l’horodatage avant de traiter l’événement.
import crypto from 'node:crypto'
export function verifyAvocat20min(req, secret) {
// En-tête : t=<timestamp>,v1=<hmac-sha256-hex>
const header = req.headers['x-avocat20min-signature'] ?? ''
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${req.rawBody}`)
.digest('hex')
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1 ?? ''),
)
// Anti-rejeu : refuser au-delà de 5 minutes
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
if (!ok || !fresh) throw new Error('Signature invalide')
}Idempotence côté récepteur
X-Avocat20min-Delivery : en cas de réessai, vous recevrez le même identifiant — ignorez les doublons.