Intégrer "Simuler avec Vaerdict"
Tuto integrateur : proposer "Calculer avec Vaerdict" depuis un site partenaire
Ce guide explique comment integrer un parcours "Calculer" depuis votre site partenaire pour creer une etude chez Vaerdict.
Objectif
- L'utilisateur final saisit ses infos sur votre site.
- Vous declenchez un calcul Vaerdict via l'API partenaire.
- Une etude est creee chez Vaerdict.
- L'utilisateur est redirige vers Vaerdict pour consulter/continuer l'etude.
Prerequis
- Un compte partenaire Vaerdict actif.
- Un Client ID et un Client Secret OAuth (fournis par Vaerdict).
- Vos redirect URIs declares (ex:
https://partner.com/vaerdict/callback). - L'URL de base Vaerdict (ex:
https://app.vaerdict.fr).
Parcours utilisateur (vue d'ensemble)
- L'utilisateur clique sur "Calculer" depuis votre site.
- Vous demarrez un flux OAuth (Authorization Code + PKCE).
- Vaerdict authentifie l'utilisateur et revient sur votre callback.
- Votre backend cree une etude via
POST /api/partner/v1/studies. - Votre backend cree une session Vaerdict et renvoie l'URL de redirection.
- Le navigateur redirige l'utilisateur vers Vaerdict.
- Si l'utilisateur revient plus tard, vous reutilisez le refresh token si disponible, sinon vous relancez OAuth.
Architecture recommandee (separation front/back)
- Front-end :
- collecte des donnees utilisateur.
- lance le flux OAuth.
- redirige vers Vaerdict a la fin.
- Back-end :
- conserve le client secret (jamais dans le navigateur).
- echange le code OAuth contre un access token.
- appelle l'API partenaire (creation d'etude, session).
Schema simplifie :
[Front partenaire] --(start OAuth)--> [Vaerdict OAuth]
| |
|<---------- callback code ----------|
| |
|-----> [Backend partenaire] --------|
- exchange token
- create study
- create session
|<------ redirect URL ---------------|
|---- redirect to Vaerdict ---------->
Etape 1 - Declarer votre application partenaire
Dans votre espace Vaerdict :
- declarez vos redirect URIs (ex:
https://partner.com/vaerdict/callback). - recuperer votre Client ID et Client Secret.
Etape 2 - Front : bouton "Calculer" (demarrage du flux)
Dans votre interface, le bouton "Calculer" peut juste rediriger vers votre backend qui initie OAuth :
// Front (ex: React/Vanilla)
const onClickCalculer = () => {
window.location.assign('/vaerdict/auth/start');
};
Votre backend va generer un state + un code_verifier (PKCE), les stocker en session, puis rediriger vers Vaerdict.
Etape 3 - Back : demarrer OAuth avec PKCE
Exemple Node/Express (a adapter) :
// Back (Express) - demarrage OAuth
import crypto from 'node:crypto';
import type {Request, Response} from 'express';
const VAERDICT_BASE_URL = "https://app.vaerdict.fr"
const CLIENT_ID = process.env.VAERDICT_CLIENT_ID!;
const base64url = (input: Buffer) =>
input
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
const sha256 = (value: string) =>
crypto.createHash('sha256').update(value).digest();
export const startVaerdictOAuth = (req: Request, res: Response) => {
const state = crypto.randomUUID();
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(sha256(codeVerifier));
// Stockage server-side (session/redis)
req.session.vaerdictOauth = {
state,
codeVerifier,
createdAt: Date.now(),
};
const redirectUri = `${req.protocol}://${req.get('host')}/vaerdict/callback`;
const url = new URL('/api/oauth/auth', VAERDICT_BASE_URL);
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('redirect_uri', redirectUri);
url.searchParams.set('state', state);
url.searchParams.set('nonce', crypto.randomUUID());
url.searchParams.set('code_challenge', codeChallenge);
url.searchParams.set('code_challenge_method', 'S256');
res.redirect(url.toString());
};
Etape 4 - Back : callback OAuth + echange de token
Sur votre route de callback, on verifie state, puis on echange le code contre un token.
// Back (Express) - callback OAuth
import type {Request, Response} from 'express';
const VAERDICT_BASE_URL = process.env.VAERDICT_BASE_URL!;
const CLIENT_ID = process.env.VAERDICT_CLIENT_ID!;
const CLIENT_SECRET = process.env.VAERDICT_CLIENT_SECRET!;
export const vaerdictCallback = async (req: Request, res: Response) => {
const {code, state, error, error_description} = req.query as Record<string, string | undefined>;
if (error) {
return res.status(400).send(error_description || error);
}
const pending = req.session.vaerdictOauth;
if (!pending || !code || pending.state !== state) {
return res.status(400).send('OAuth state invalid.');
}
const redirectUri = `${req.protocol}://${req.get('host')}/vaerdict/callback`;
const tokenUrl = new URL('/api/oauth/token', VAERDICT_BASE_URL);
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: CLIENT_ID,
code_verifier: pending.codeVerifier,
});
const tokenRes = await fetch(tokenUrl.toString(), {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: body.toString(),
});
if (!tokenRes.ok) {
return res.status(502).send('Token exchange failed.');
}
const tokens = (await tokenRes.json()) as {
access_token: string;
refresh_token?: string;
expires_in?: number;
};
// Stockage serveur (session/DB) pour reutilisation
req.session.vaerdictToken = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token ?? null,
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null,
};
// Continuer : creation d'etude + session Vaerdict
const redirectUrl = await createVaerdictStudyAndSession(req);
return res.redirect(redirectUrl);
};
Si l'utilisateur revient le lendemain pour un nouveau calcul, votre access token sera probablement expire.
Deux options :
- Rejouer OAuth (simple, fiable).
- Utiliser un refresh token (si Vaerdict en delivre dans votre configuration).
Quand on recoit le refresh token
Le refresh token arrive dans la reponse de l'echange OAuth (Etape 4).
Quand il est present :
- stockez-le cote serveur (jamais dans le navigateur),
- associez-le a l'utilisateur/compte partenaire,
- chiffre-le au repos (KMS/secret manager/DB encryption),
- conservez une date de derniere utilisation pour audit.
Exemple de stockage minimal :
await db.partnerTokens.upsert({
partnerUserId,
accessToken: tokens.access_token,
accessTokenExpiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null,
refreshToken: tokens.refresh_token ?? null,
refreshTokenUpdatedAt: new Date(),
});
Quand le reutiliser
Lorsqu'un utilisateur revient pour un nouveau calcul :
- Si access token encore valide -> utilisez-le.
- Sinon, si refresh token existe -> faites un refresh.
- Si refresh token invalide/expire -> relancez OAuth.
Exemple de refresh token (si disponible)
Si le refresh token est actif, implementez le flow suivant :
// Back (Express) - refresh token (si disponible)
const refreshAccessToken = async (refreshToken: string) => {
const tokenUrl = new URL('/api/oauth/token', VAERDICT_BASE_URL);
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
});
const response = await fetch(tokenUrl.toString(), {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: body.toString(),
});
if (!response.ok) {
throw new Error('Refresh token failed');
}
return (await response.json()) as {
access_token: string;
refresh_token?: string;
expires_in?: number;
};
};
Logique recommandee :
- Si access token valide -> creer l'etude.
- Sinon, tenter refresh token.
- Si refresh token invalide/expire -> relancer OAuth.
Parcours utilisateur
- L'utilisateur revient sur votre site et clique "Calculer".
- Votre backend verifie le token stocke pour cet utilisateur.
- Si refresh OK, vous creez l'etude sans forcer une reconnexion.
- Si refresh KO, l'utilisateur repasse par l'ecran Vaerdict de login.
Etape 5 - Back : creation d'etude (API partenaire)
Endpoint utilise : POST /api/partner/v1/studies
Points importants :
- Header
Authorization: Bearer <access_token> - Header
Idempotency-Keyunique par requete (si fourni, il doit etre egal aclientRequestId) clientRequestIddans le payload
Exemple :
const createStudy = async ({accessToken, payload}: {accessToken: string; payload: PartnerCreateStudyPayload}) => {
const idempotencyKey = payload.clientRequestId;
const response = await fetch(new URL('/api/partner/v1/studies', VAERDICT_BASE_URL).toString(), {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${accessToken}`,
'idempotency-key': idempotencyKey,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || 'Create study failed');
}
return (await response.json()) as {
isIdempotent: boolean;
investorId: number;
investmentId: number;
propertyId: number;
createdAt: string;
};
};
Exemple de payload (a adapter)
export type PartnerCreateStudyPayload = {
clientRequestId: string;
investor: {
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
};
simulation: {
projectType: 'new-project' | 'existing-purchase' | null;
isNewBuild: boolean;
isOffPlanSale: boolean;
isServicedAccommodation: boolean;
investmentFramework:
| 'monument-historique'
| 'regime-general-location-nue'
| 'deficit-foncier'
| 'pinel'
| 'pinel-plus'
| 'pinel-outre-mer'
| 'lmnp'
| 'lli-sci-is'
| 'lli-sci-ir'
| 'lmnp-residence-senior'
| 'censi-bouvard'
| 'pinel-denormandie'
| 'malraux'
| 'locavantage';
isMicroRegime: boolean;
financingType: 'loan' | 'cash';
studyName: string;
mode: 'simple' | 'detailed';
propertyType: 'house' | 'apartment';
surfaceAreaSqm: number;
purchasePrice: number;
notaryFees: number;
renovationCost: number;
furnitureCost: number;
monthlyRent: number;
annualPropertyTax: number;
annualAccountingFee: number;
annualPropertyManagementFees: number;
taxReductionRate: number;
annualNonOccupantOwnerInsurance: number;
resalePrice: number;
annualCondominiumFees: number;
rentalType: 'unfurnished-rental' | 'furnished-rental' | null;
propertyCity: string;
propertyStreet: string;
downPaymentAmount: number;
loanTermYears: number;
loanInterestRate: number;
deedSigningYear: number | null;
programName?: string | null;
lotDesignation?: string | null;
projectStage: 'property-found' | 'active-search' | 'assessing-suitability' | null;
objective: 'hold-or-sell' | 'verify-performance' | 'optimize-strategy' | null;
};
investorProfile: {
annualIncome: number;
maritalStatus: 'single' | 'married';
numberOfChildren: number;
taxHouseholdParts: number;
annualRentalIncome: number;
annualBicIncome: number;
};
metadata?: Record<string, unknown>;
};
Etape 6 - Back : creation d'une session Vaerdict (SSO)
Pour rediriger l'utilisateur vers Vaerdict sans friction, creez une session partenaire :
const createPartnerSession = async ({subject, redirectTo}: {subject: string; redirectTo: string}) => {
const response = await fetch(new URL('/api/partner/v1/sessions', VAERDICT_BASE_URL).toString(), {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: JSON.stringify({subject, redirectTo}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || 'Partner session creation failed');
}
return (await response.json()) as {redirectUrl?: string};
};
subjectest l'identifiant utilisateur issu du token OAuth (subdans le JWT).redirectToest le chemin cible dans Vaerdict (doit commencer par/et ne pas commencer par//).
Exemple de redirect vers l'etude creee :
/app/investors/investment/:investmentId/characteristics/:propertyId
Etape 7 - Front : redirection finale
Une fois redirectUrl recu du backend, redirigez :
window.location.assign(redirectUrl);
Securite (points essentiels)
- Client Secret uniquement cote serveur.
- Utiliser PKCE,
stateetnonce, et verifierstateau callback. - Stocker les tokens en session server-side (pas de localStorage en prod).
- Utiliser un Idempotency-Key pour eviter les doublons.
- N'accepter que des redirect URIs whitelistees.
- HTTPS + cookies HttpOnly pour les sessions.
- Loguer sans PII sensible (pas d'email complet, pas de token).
Checklist avant go-live
- Redirect URIs valides et testees.
- Client secret stocke dans un secret manager.
- Flux OAuth complet (auth -> callback -> create study -> session).
- Gestion des erreurs et des retries.
- Tests end-to-end avec un compte de test Vaerdict.
Mis à jour le : 02/02/2026
Merci !