Maintenance contracts
Recurring maintenance commitments (yearly CV-ketel check, quarterly VvE round, custom intervals) between your business and a customer. Each contract can have one or more installations (CV-ketel, warmtepomp, airco, lift, etc). An auto-generate cron creates work orders ahead of each scheduled visit based on the contract's frequency.
Scopes: operations.maintenance.read / operations.maintenance.write / operations.maintenance.delete.
403 MODULE_VAKMAN_DISABLED otherwise, even when the API key carries the right scopes.Endpoints
/api/workspace/v1/maintenance-contracts/api/workspace/v1/maintenance-contracts/{id}/api/workspace/v1/maintenance-contracts/api/workspace/v1/maintenance-contracts/{id}/api/workspace/v1/maintenance-contracts/{id}/api/workspace/v1/maintenance-contracts/{id}/restore/api/workspace/v1/maintenance-contracts/{id}/installations/api/workspace/v1/maintenance-contracts/{id}/installations/api/workspace/v1/maintenance-contracts/{id}/installations/{installationId}The maintenance contract object
| Field | Type | Description |
|---|---|---|
| contract_number | string | Auto-generated, e.g. MC-2026-00001 |
| contract_name | string | Human-readable label (required) |
| hub_customer_id | integer | Advanced Booking customer id — one of hub_customer_id or crm_contact_id is required on create |
| crm_contact_id | integer | CRM contact id — alternative to hub_customer_id (auto-bridges to a hub customer) |
| frequency | enum | weekly | monthly | quarterly | semi_annually | yearly | custom_days |
| custom_interval_days | integer | Required when frequency = custom_days (e.g. 90 = every 90 days) |
| service_description | string | What the service covers (e.g. CV-ketel onderhoudsbeurt) |
| price_per_visit | number | Fixed price in EUR per auto-generated visit |
| duration_minutes | integer | Expected visit duration (default 60) |
| assigned_team_member_id | integer | Default technician for auto-generated work orders |
| starts_at | string(ISO) | Start date of the contract (required) |
| ends_at | string(ISO) | Optional end date — null = runs indefinitely |
| next_generate_at | string(ISO) | When the auto-generate cron will next create a work order |
| last_generated_at | string(ISO) | When the last auto-generated work order was created |
| last_generated_workorder_id | integer | id of the most recent auto-generated work order |
| active | boolean | Paused if false — auto-generate stops |
| created_at | string(ISO) | When the contract was created |
| deleted_at | string(ISO) | Soft-delete timestamp — null if active |
Create a contract
curl "https://app.staffifyai.com/api/workspace/v1/maintenance-contracts" \
-X POST \
-H "Authorization: Bearer sfy_wsp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"contract_name": "Jaarlijks onderhoud VvE Lange Vijverberg",
"crm_contact_id": 1893,
"frequency": "yearly",
"service_description": "APK CV-ketel + legionellacontrole",
"price_per_visit": 165.00,
"duration_minutes": 90,
"starts_at": "2026-11-01T09:00:00+01:00"
}'Installations (nested)
Each contract can list the physical units it covers. Installations use the same operations.maintenance scopes as the parent contract — there is no separate scope.
| Field | Type | Description |
|---|---|---|
| contract_id | integer | Parent contract id |
| installation_name | string | Short label (e.g. "CV Vaillant TurboTec Pro") |
| installation_type | string | Category (e.g. "CV-ketel", "warmtepomp", "airco") |
| serial_number | string | Manufacturer serial for warranty/compliance |
| installation_date | string(ISO) | When the unit was installed on-site |
| notes | string | Technician notes (max 2000 chars) |
curl "https://app.staffifyai.com/api/workspace/v1/maintenance-contracts/42/installations" \
-X POST \
-H "Authorization: Bearer sfy_wsp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"installation_name": "CV Vaillant TurboTec Pro",
"installation_type": "CV-ketel",
"serial_number": "VLT-2021-884433"
}'Soft-delete and restore
DELETE moves the contract to the 30-day recycle bin by setting deleted_at. Default list queries exclude deleted rows; append ?deleted=true to see only deleted or ?deleted=any to see both. POST to /{id}/restore lifts the deletion. After 30 days the purge cron hard-deletes the row and all installations under it.
Events
maintenance_contract.createdmaintenance_contract.updatedmaintenance_contract.deletedmaintenance_contract.restoredmaintenance_installation.addedmaintenance_installation.removed