10. Rental contracts
Two paths exist depending on whether the contract is generated via PandaDoc or imported as an already-signed PDF.
| Path | When to use | Main mutations |
|---|---|---|
| PandaDoc generation | Create a contract for electronic signature | generateRentalContract, createPandadocDraftSession, sendGeneratedRentalContract |
| Signed PDF import | Add an already-signed contract from outside Ublo | uploadImportedContractDocumentFile, updateImportedRentalContract |
Both paths require the organization feature flag ENABLE_CONTRACT_V2. Without it, V2 mutations return: Contract V2 feature not enabled for this organization.
Part A — Generation & send (PandaDoc)
Observed — contract generation, PandaDoc draft session, send to signatories.
Why
Produce a rental contract from a folder, open the e-sign draft/editor, then send the generated document.
Prerequisites
- Rental folder (
rentalFolderId) - Contract template (
contractTemplateId) - Signatories (
personalProfileId/ roles) and representative ENABLE_CONTRACT_V2feature flag active
10.1 Generate (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 from the GraphQL tab
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 PandaDoc draft session (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 Send the contract (sendGeneratedRentalContract)
mutation sendGeneratedRentalContract($rentalContractId: UUID!) {
sendGeneratedRentalContract(rentalContractId: $rentalContractId) {
id
state
contractStatus
}
}
{
"rentalContractId": "00000000-0000-4000-8000-000000000106"
}
Part B — Import a signed contract
Reconstructed — PDF import workflow via UploadCare, aligned with the public GraphQL schema.
Why
Add an already-signed rental contract (external PDF) to a folder with key dates and signatories, without going through PandaDoc.
Prerequisites
- Organization feature flag
ENABLE_CONTRACT_V2active - Existing rental folder (
rentalFolderId) - PDF already uploaded to UploadCare (
foreignIdreturned by UploadCare) - At least one signatory in
signatories(personal or moral profile + role) - Tenant profiles / collaborators already created in the folder
Workflow
Distinction from the legacy flow
| Mutation | Usage | Status with ENABLE_CONTRACT_V2 |
|---|---|---|
uploadImportedContractDocumentFile | Creates a new imported contract + document + signatories in one atomic operation | Use this for import |
uploadContractDocumentFile | Attaches an UploadCare document to an existing contract (legacy workflow) | Deprecated — returns This endpoint is deprecated when Contract V2 is enabled |
generateRentalContract | Generates a PandaDoc contract for signature | Generation path (part A) |
10.4 Import (uploadImportedContractDocumentFile)
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
rentalFolderId | UUID! | Yes | Target rental folder |
input | UploadContractDocumentFileInput! | Yes | Document, key dates, signatories |
Input UploadContractDocumentFileInput
| Field | Type | Required | Description |
|---|---|---|---|
foreignId | NonEmptyString! | Yes | UploadCare file identifier |
mimeType | String | No | e.g. application/pdf |
name | String | No | Display name of the document |
description | String | No | Document description |
signedAt | Date! | Yes | Contract signature date |
isAmendment | Boolean! | Yes | false = contract, true = amendment |
contractStartDate | Date! | Yes | Effective start date |
contractEndDate | Date! | Yes | End date |
durationInMonths | Int! | Yes | Initial duration in months |
renewDurationInMonths | Int! | Yes | Renewal duration in months |
leaseRenewalOption | LeaseRenewalOptionTypeEnum! | Yes | automatic_renewal, limited_renewal_number, no_renewal |
leaseRenewalLimit | Int | Conditional | Required when leaseRenewalOption = limited_renewal_number |
terminationOptionDates | [Date!] | No | Notice option dates (commercial lease) |
signatories | [RentalContractSignatoryInput!]! | Yes | At least one signatory |
signatoryContractRepresentativeId | UUID | No | Collaborator signatory (landlord representative) |
Input RentalContractSignatoryInput
| Field | Type | Description |
|---|---|---|
personalProfileId | UUID | Personal profile (tenant, guarantor…) |
moralProfileId | UUID | Moral profile |
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": "signed-lease.pdf",
"description": "Lease signed outside 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: 'signed-lease.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 from the GraphQL tab
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
Automatically set values
The API automatically sets the following fields without exposing them as input:
| Field | Value | Detail |
|---|---|---|
contractType | imported | Distinct from generated (PandaDoc) |
contractStatus | signed | Already signed contract |
documents[].provider | uploadcare | UploadCare storage |
documents[].state | completed | Finalized document |
documents[].type | contract | Document type |
signatories[].status | completed | All signatories have signed |
signatories[].signedAt | equal to contract signedAt | Shared signature date |
reference | auto-generated | CR prefix, e.g. CR00042 |
Side effects
- Contract key dates locked
- Search indexing (TypeSense)
Common errors
| Message | Likely cause |
|---|---|
Contract V2 feature not enabled for this organization | ENABLE_CONTRACT_V2 feature flag inactive on the organization |
This endpoint is deprecated when Contract V2 is enabled | Using legacy uploadContractDocumentFile instead of uploadImportedContractDocumentFile |
Validation error on signedAt | Required field missing from input |
Validation error on signatories | Empty list or invalid profiles |
10.5 Update an imported contract (updateImportedRentalContract)
Use this mutation to correct key dates, document type (isAmendment), signatories, or replace the PDF file.
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
id | UUID! | Yes | Imported RentalContract ID |
input | UpdateImportedRentalContractInput! | Yes | Editable fields |
Required fields in UpdateImportedRentalContractInput match the import (contractStartDate, contractEndDate, durationInMonths, renewDurationInMonths, leaseRenewalOption, isAmendment, signatories). Document fields (foreignId, mimeType, name, description) and signedAt are optional: omitting them leaves the existing value unchanged.
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 only applies to imported contracts. generated contracts (PandaDoc) follow a different lifecycle.
Observed subscription
The capture also subscribed to RentalContractChanged (contract state tracking). No full example here — see the Subscription schema.