10. Contrats de location
Deux parcours coexistent selon que le contrat est généré via PandaDoc ou importé en PDF déjà signé.
| Parcours | Quand l'utiliser | Mutations principales |
|---|---|---|
| Génération PandaDoc | Créer un contrat à faire signer électroniquement | generateRentalContract, createPandadocDraftSession, sendGeneratedRentalContract |
| Import PDF signé | Ajouter un contrat déjà signé hors Ublo | uploadImportedContractDocumentFile, updateImportedRentalContract |
Les deux parcours requièrent le feature flag organisation ENABLE_CONTRACT_V2. Sans ce flag, les mutations V2 renvoient : Contract V2 feature not enabled for this organization.
Partie A — Génération & envoi (PandaDoc)
Observé — génération de contrat, session brouillon PandaDoc, envoi aux signataires.
Pourquoi
Produire un contrat de location à partir d'un dossier, ouvrir l'éditeur / brouillon e-signature, puis envoyer le document généré.
Prérequis
- Dossier de location (
rentalFolderId) - Modèle de contrat (
contractTemplateId) - Signataires (
personalProfileId/ rôles) et représentant - Feature flag
ENABLE_CONTRACT_V2actif
10.1 Générer (generateRentalContract)
- GraphQL
- Node.js
- curl
mutation generateRentalContract($rentalFolderId: UUID!, $input: GenerateRentalContractInput) {
generateRentalContract(rentalFolderId: $rentalFolderId, input: $input) {
id
state
contractStatus
contractType
}
}
{
"rentalFolderId": "00000000-0000-4000-8000-000000000093",
"input": {
"contractTemplateId": "00000000-0000-4000-8000-000000000104",
"isAmendment": false,
"contractStartDate": "2026-08-03",
"contractEndDate": "2029-08-02",
"durationInMonths": 36,
"leaseRenewalOption": "limited_renewal_number",
"renewDurationInMonths": 36,
"leaseRenewalLimit": 1,
"signatoryContractRepresentativeId": "00000000-0000-4000-8000-000000000105",
"signatories": [
{
"role": "tenant",
"personalProfileId": "00000000-0000-4000-8000-000000000099"
}
]
}
}
import { ublo } from '../ubloClient.mjs';
const { generateRentalContract: contract } = await ublo(
`mutation generateRentalContract($rentalFolderId: UUID!, $input: GenerateRentalContractInput) {
generateRentalContract(rentalFolderId: $rentalFolderId, input: $input) {
id
state
contractStatus
contractType
}
}`,
{
rentalFolderId: '00000000-0000-4000-8000-000000000093',
input: {
contractTemplateId: '00000000-0000-4000-8000-000000000104',
isAmendment: false,
contractStartDate: '2026-08-03',
contractEndDate: '2029-08-02',
durationInMonths: 36,
leaseRenewalOption: 'limited_renewal_number',
renewDurationInMonths: 36,
leaseRenewalLimit: 1,
signatoryContractRepresentativeId: '00000000-0000-4000-8000-000000000105',
signatories: [
{
role: 'tenant',
personalProfileId: '00000000-0000-4000-8000-000000000099',
},
],
},
},
);
const rentalContractId = contract.id;
- Bearer
- Cookie session
# payload.json = query + variables de l'onglet GraphQL
curl -sS -X POST "$UBLO_GRAPHQL_URL" \
-H "Authorization: Bearer $UBLO_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @payload.json
curl -sS -X POST "$UBLO_GRAPHQL_URL" \
-H 'Content-Type: application/json' \
-b cookies.txt \
-d @payload.json
10.2 Session brouillon PandaDoc (createPandadocDraftSession)
mutation createPandadocDraftSession($rentalContractId: UUID!) {
createPandadocDraftSession(rentalContractId: $rentalContractId) {
eToken
expiresAt
}
}
{
"rentalContractId": "00000000-0000-4000-8000-000000000106"
}
- Bearer
- Cookie session
curl -sS -X POST "$UBLO_GRAPHQL_URL" \
-H "Authorization: Bearer $UBLO_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"mutation createPandadocDraftSession($rentalContractId: UUID!) { createPandadocDraftSession(rentalContractId: $rentalContractId) { eToken expiresAt } }","variables":{"rentalContractId":"00000000-0000-4000-8000-000000000106"}}'
curl -sS -X POST "$UBLO_GRAPHQL_URL" \
-H 'Content-Type: application/json' \
-b cookies.txt \
-d '{"query":"mutation createPandadocDraftSession($rentalContractId: UUID!) { createPandadocDraftSession(rentalContractId: $rentalContractId) { eToken expiresAt } }","variables":{"rentalContractId":"00000000-0000-4000-8000-000000000106"}}'
10.3 Envoyer le contrat (sendGeneratedRentalContract)
mutation sendGeneratedRentalContract($rentalContractId: UUID!) {
sendGeneratedRentalContract(rentalContractId: $rentalContractId) {
id
state
contractStatus
}
}
{
"rentalContractId": "00000000-0000-4000-8000-000000000106"
}
Partie B — Import d'un contrat signé
Reconstruit — workflow import PDF via UploadCare, aligné sur le schéma GraphQL public.
Pourquoi
Ajouter au dossier un contrat de location déjà signé (PDF externe), avec ses dates clés et signataires, sans passer par PandaDoc.
Prérequis
- Feature flag organisation
ENABLE_CONTRACT_V2actif - Dossier locatif existant (
rentalFolderId) - PDF déjà uploadé sur UploadCare (
foreignIdretourné par UploadCare) - Au moins un signataire dans
signatories(profil physique ou moral + rôle) - Profils locataires / collaborateurs déjà créés dans le dossier
Workflow
Distinction avec le flux legacy
| Mutation | Usage | Statut avec ENABLE_CONTRACT_V2 |
|---|---|---|
uploadImportedContractDocumentFile | Crée un nouveau contrat importé + document + signataires en une seule opération atomique | A utiliser pour l'import |
uploadContractDocumentFile | Attache un document UploadCare à un contrat existant (workflow legacy) | Deprecie — renvoie This endpoint is deprecated when Contract V2 is enabled |
generateRentalContract | Génère un contrat PandaDoc à faire signer | Parcours génération (partie A) |
10.4 Importer (uploadImportedContractDocumentFile)
Arguments
| Argument | Type | Obligatoire | Description |
|---|---|---|---|
rentalFolderId | UUID! | Oui | Dossier locatif cible |
input | UploadContractDocumentFileInput! | Oui | Document, dates clés, signataires |
Input UploadContractDocumentFileInput
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
foreignId | NonEmptyString! | Oui | Identifiant UploadCare du PDF |
mimeType | String | Non | Ex. application/pdf |
name | String | Non | Nom affiché du document |
description | String | Non | Description du document |
signedAt | Date! | Oui | Date de signature du contrat |
isAmendment | Boolean! | Oui | false = contrat, true = avenant |
contractStartDate | Date! | Oui | Prise d'effet |
contractEndDate | Date! | Oui | Fin de validité |
durationInMonths | Int! | Oui | Durée initiale en mois |
renewDurationInMonths | Int! | Oui | Durée d'une reconduction |
leaseRenewalOption | LeaseRenewalOptionTypeEnum! | Oui | automatic_renewal, limited_renewal_number, no_renewal |
leaseRenewalLimit | Int | Conditionnel | Requis si leaseRenewalOption = limited_renewal_number |
terminationOptionDates | [Date!] | Non | Options de congé (bail commercial) |
signatories | [RentalContractSignatoryInput!]! | Oui | Au moins un signataire |
signatoryContractRepresentativeId | UUID | Non | Collaborateur signataire (représentant bailleur) |
Input RentalContractSignatoryInput
| Champ | Type | Description |
|---|---|---|
personalProfileId | UUID | Profil physique (locataire, garant…) |
moralProfileId | UUID | Profil moral |
role | RentalContractSignatoryRoleEnum | tenant, guarantor, proxy |
- GraphQL
- Node.js
- curl
mutation uploadImportedContractDocumentFile(
$rentalFolderId: UUID!
$input: UploadContractDocumentFileInput!
) {
uploadImportedContractDocumentFile(rentalFolderId: $rentalFolderId, input: $input) {
id
reference
contractType
contractStatus
isAmendment
signedAt
contractStartDate
contractEndDate
durationInMonths
renewDurationInMonths
leaseRenewalOption
leaseRenewalLimit
terminationOptionDates
signatoryContractRepresentativeId
documents {
... on UploadcareFile {
id
foreignId
provider
state
type
mimeType
name
}
}
signatories {
id
role
status
signedAt
personalProfileId
moralProfileId
}
}
}
{
"rentalFolderId": "00000000-0000-4000-8000-000000000093",
"input": {
"foreignId": "UPLOADCARE_FILE_ID_PLACEHOLDER",
"mimeType": "application/pdf",
"name": "bail-signe.pdf",
"description": "Bail signe hors Ublo",
"signedAt": "2026-01-15",
"isAmendment": false,
"contractStartDate": "2026-02-01",
"contractEndDate": "2029-01-31",
"durationInMonths": 36,
"renewDurationInMonths": 36,
"leaseRenewalOption": "limited_renewal_number",
"leaseRenewalLimit": 1,
"terminationOptionDates": ["2028-07-31"],
"signatoryContractRepresentativeId": "00000000-0000-4000-8000-000000000105",
"signatories": [
{
"role": "tenant",
"personalProfileId": "00000000-0000-4000-8000-000000000099"
},
{
"role": "guarantor",
"personalProfileId": "00000000-0000-4000-8000-000000000110"
}
]
}
}
import { ublo } from '../ubloClient.mjs';
const { uploadImportedContractDocumentFile: contract } = await ublo(
`mutation uploadImportedContractDocumentFile(
$rentalFolderId: UUID!
$input: UploadContractDocumentFileInput!
) {
uploadImportedContractDocumentFile(rentalFolderId: $rentalFolderId, input: $input) {
id
reference
contractType
contractStatus
signedAt
}
}`,
{
rentalFolderId: '00000000-0000-4000-8000-000000000093',
input: {
foreignId: 'UPLOADCARE_FILE_ID_PLACEHOLDER',
mimeType: 'application/pdf',
name: 'bail-signe.pdf',
signedAt: '2026-01-15',
isAmendment: false,
contractStartDate: '2026-02-01',
contractEndDate: '2029-01-31',
durationInMonths: 36,
renewDurationInMonths: 36,
leaseRenewalOption: 'limited_renewal_number',
leaseRenewalLimit: 1,
signatoryContractRepresentativeId: '00000000-0000-4000-8000-000000000105',
signatories: [
{
role: 'tenant',
personalProfileId: '00000000-0000-4000-8000-000000000099',
},
],
},
},
);
const rentalContractId = contract.id;
- Bearer
- Cookie session
# payload-import.json = query + variables de l'onglet GraphQL
curl -sS -X POST "$UBLO_GRAPHQL_URL" \
-H "Authorization: Bearer $UBLO_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @payload-import.json
curl -sS -X POST "$UBLO_GRAPHQL_URL" \
-H 'Content-Type: application/json' \
-b cookies.txt \
-d @payload-import.json
Valeurs definies automatiquement
L'API renseigne automatiquement les champs suivants sans les exposer en input :
| Champ | Valeur | Detail |
|---|---|---|
contractType | imported | Distinction avec generated (PandaDoc) |
contractStatus | signed | Contrat deja signe |
documents[].provider | uploadcare | Stockage UploadCare |
documents[].state | completed | Document finalise |
documents[].type | contract | Type document |
signatories[].status | completed | Tous les signataires ont signe |
signatories[].signedAt | egal a signedAt du contrat | Date de signature commune |
reference | generee automatiquement | Prefixe CR, ex. CR00042 |
Effets de bord
- Verrouillage des dates cles du contrat
- Indexation pour la recherche (TypeSense)
Erreurs courantes
| Message | Cause probable |
|---|---|
Contract V2 feature not enabled for this organization | Feature flag ENABLE_CONTRACT_V2 inactif sur l'organisation |
This endpoint is deprecated when Contract V2 is enabled | Utilisation de uploadContractDocumentFile (legacy) au lieu de uploadImportedContractDocumentFile |
Erreur de validation sur signedAt | Champ obligatoire manquant dans l'input |
Erreur de validation sur signatories | Liste vide ou profils invalides |
10.5 Modifier un contrat importe (updateImportedRentalContract)
Utiliser cette mutation pour corriger les dates cles, le type de document (isAmendment), les signataires ou remplacer le fichier PDF.
Arguments
| Argument | Type | Obligatoire | Description |
|---|---|---|---|
id | UUID! | Oui | ID du RentalContract importe |
input | UpdateImportedRentalContractInput! | Oui | Champs modifiables |
Les champs obligatoires de UpdateImportedRentalContractInput sont les memes que pour l'import (contractStartDate, contractEndDate, durationInMonths, renewDurationInMonths, leaseRenewalOption, isAmendment, signatories). Les champs document (foreignId, mimeType, name, description) et signedAt sont optionnels : les omettre laisse la valeur existante inchangee.
mutation updateImportedRentalContract(
$id: UUID!
$input: UpdateImportedRentalContractInput!
) {
updateImportedRentalContract(id: $id, input: $input) {
id
contractType
contractStatus
isAmendment
signedAt
contractStartDate
contractEndDate
durationInMonths
renewDurationInMonths
leaseRenewalOption
leaseRenewalLimit
terminationOptionDates
signatoryContractRepresentativeId
documents {
... on UploadcareFile {
id
foreignId
name
}
}
signatories {
id
role
personalProfileId
moralProfileId
}
}
}
{
"id": "00000000-0000-4000-8000-000000000106",
"input": {
"contractStartDate": "2026-03-01",
"contractEndDate": "2029-02-28",
"durationInMonths": 36,
"renewDurationInMonths": 36,
"leaseRenewalOption": "automatic_renewal",
"isAmendment": true,
"signedAt": "2026-02-20",
"signatoryContractRepresentativeId": "00000000-0000-4000-8000-000000000105",
"signatories": [
{
"role": "tenant",
"personalProfileId": "00000000-0000-4000-8000-000000000099"
}
]
}
}
updateImportedRentalContract ne s'applique qu'aux contrats de type imported. Les contrats generated (PandaDoc) suivent un autre cycle de vie.
Subscription observee
La capture a egalement souscrit a RentalContractChanged (suivi d'etat du contrat). Pas d'exemple complet ici — voir le schema Subscription.