@serviceai/api-spec 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.yaml ADDED
@@ -0,0 +1,2735 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: ServiceAi API
4
+ version: 1.0.7
5
+ description: >-
6
+ Source-of-truth API contract for ServiceAi. Generated from `shared/schema.ts` Zod schemas by
7
+ `scripts/build-api-spec.ts`. Versioning policy: see `docs/api-versioning.md`. The hand-written docs/ios-contract.md
8
+ and docs/lidar-mobile-upload-contract.md are human-readable design notes only — this spec is the canonical wire
9
+ contract.
10
+ license:
11
+ name: Proprietary
12
+ servers:
13
+ - url: https://app.serviceai.com
14
+ description: Production
15
+ - url: http://localhost:5000
16
+ description: Local development
17
+ tags:
18
+ - name: version
19
+ description: Handshake + diagnostics.
20
+ - name: lidar
21
+ description: iOS LiDAR / RoomPlan ingest + floor management.
22
+ - name: auth
23
+ description: Authentication + capability discovery.
24
+ - name: features
25
+ description: Feature flag surface.
26
+ - name: crm
27
+ description: Contacts + CRM surface.
28
+ - name: photos
29
+ description: Photo / asset upload surface.
30
+ - name: jobs
31
+ description: Job (project) lifecycle surface.
32
+ - name: chatty
33
+ description: Realtime voice-enabled AI assistant.
34
+ components:
35
+ schemas:
36
+ ErrorEnvelope:
37
+ type: object
38
+ properties:
39
+ error:
40
+ type: string
41
+ example: validation_failed
42
+ message:
43
+ type: string
44
+ example: roomPlanJSON is required
45
+ details: {}
46
+ required:
47
+ - error
48
+ - message
49
+ description: Structured error response returned by every endpoint on failure.
50
+ UpgradeRequiredEnvelope:
51
+ type: object
52
+ properties:
53
+ error:
54
+ type: string
55
+ enum:
56
+ - upgrade_required
57
+ message:
58
+ type: string
59
+ apiVersion:
60
+ type: string
61
+ minClientVersion:
62
+ type: string
63
+ deprecatedBelow:
64
+ type: string
65
+ clientVersion:
66
+ type: string
67
+ required:
68
+ - error
69
+ - message
70
+ - apiVersion
71
+ - minClientVersion
72
+ - deprecatedBelow
73
+ - clientVersion
74
+ description: Returned with HTTP 426 when the X-Client-Version major is below minClientVersion.
75
+ VersionManifest:
76
+ type: object
77
+ properties:
78
+ apiVersion:
79
+ type: string
80
+ example: 1.0.7
81
+ minClientVersion:
82
+ type: string
83
+ example: 1.0.0
84
+ deprecatedBelow:
85
+ type: string
86
+ example: 1.0.0
87
+ version:
88
+ type: string
89
+ description: Legacy alias for apiVersion.
90
+ buildTime:
91
+ type: string
92
+ env:
93
+ type:
94
+ - string
95
+ - 'null'
96
+ gitCommit:
97
+ type: string
98
+ required:
99
+ - apiVersion
100
+ - minClientVersion
101
+ - deprecatedBelow
102
+ - version
103
+ - buildTime
104
+ - gitCommit
105
+ description: Cross-platform version handshake manifest. See docs/api-versioning.md.
106
+ InsertContact:
107
+ type: object
108
+ properties:
109
+ userId:
110
+ type:
111
+ - string
112
+ - 'null'
113
+ organizationId:
114
+ type:
115
+ - string
116
+ - 'null'
117
+ customerNumber:
118
+ type:
119
+ - integer
120
+ - 'null'
121
+ minimum: -2147483648
122
+ maximum: 2147483647
123
+ name:
124
+ type: string
125
+ email:
126
+ type:
127
+ - string
128
+ - 'null'
129
+ phone:
130
+ type:
131
+ - string
132
+ - 'null'
133
+ company:
134
+ type:
135
+ - string
136
+ - 'null'
137
+ address:
138
+ type:
139
+ - string
140
+ - 'null'
141
+ status:
142
+ type:
143
+ - string
144
+ - 'null'
145
+ customFields:
146
+ anyOf:
147
+ - type: string
148
+ - type: number
149
+ - type: boolean
150
+ - type: 'null'
151
+ - type: object
152
+ additionalProperties: {}
153
+ - type: array
154
+ items: {}
155
+ - type: 'null'
156
+ lastContactDate:
157
+ type:
158
+ - string
159
+ - 'null'
160
+ parentContactId:
161
+ type:
162
+ - integer
163
+ - 'null'
164
+ minimum: -2147483648
165
+ maximum: 2147483647
166
+ relationship:
167
+ type:
168
+ - string
169
+ - 'null'
170
+ role:
171
+ type:
172
+ - string
173
+ - 'null'
174
+ firstName:
175
+ type:
176
+ - string
177
+ - 'null'
178
+ lastName:
179
+ type:
180
+ - string
181
+ - 'null'
182
+ serviceAddress:
183
+ type:
184
+ - string
185
+ - 'null'
186
+ billingAddress:
187
+ type:
188
+ - string
189
+ - 'null'
190
+ isCommercial:
191
+ type:
192
+ - boolean
193
+ - 'null'
194
+ pointOfContactName:
195
+ type:
196
+ - string
197
+ - 'null'
198
+ pointOfContactTitle:
199
+ type:
200
+ - string
201
+ - 'null'
202
+ pointOfContactPhone:
203
+ type:
204
+ - string
205
+ - 'null'
206
+ primaryEmail:
207
+ type:
208
+ - string
209
+ - 'null'
210
+ propertyAddress:
211
+ type:
212
+ - string
213
+ - 'null'
214
+ leadSource:
215
+ type:
216
+ - string
217
+ - 'null'
218
+ leadSourceReferralId:
219
+ type:
220
+ - integer
221
+ - 'null'
222
+ minimum: -2147483648
223
+ maximum: 2147483647
224
+ leadSourceReferralUserId:
225
+ type:
226
+ - string
227
+ - 'null'
228
+ leadSourceOther:
229
+ type:
230
+ - string
231
+ - 'null'
232
+ googleDriveCustomerFolderId:
233
+ type:
234
+ - string
235
+ - 'null'
236
+ cachedLat:
237
+ type:
238
+ - number
239
+ - 'null'
240
+ minimum: -140737488355328
241
+ maximum: 140737488355327
242
+ cachedLng:
243
+ type:
244
+ - number
245
+ - 'null'
246
+ minimum: -140737488355328
247
+ maximum: 140737488355327
248
+ qboId:
249
+ type:
250
+ - string
251
+ - 'null'
252
+ qboSyncToken:
253
+ type:
254
+ - string
255
+ - 'null'
256
+ qboLastPushedAt:
257
+ type:
258
+ - string
259
+ - 'null'
260
+ qboLastPushedHash:
261
+ type:
262
+ - string
263
+ - 'null'
264
+ qboLastError:
265
+ type:
266
+ - string
267
+ - 'null'
268
+ deletedAt:
269
+ type:
270
+ - string
271
+ - 'null'
272
+ deletedBy:
273
+ type:
274
+ - string
275
+ - 'null'
276
+ required:
277
+ - name
278
+ InsertProperty:
279
+ type: object
280
+ properties:
281
+ organizationId:
282
+ type: string
283
+ jobId:
284
+ type:
285
+ - integer
286
+ - 'null'
287
+ minimum: -2147483648
288
+ maximum: 2147483647
289
+ name:
290
+ type: string
291
+ address1:
292
+ type:
293
+ - string
294
+ - 'null'
295
+ address2:
296
+ type:
297
+ - string
298
+ - 'null'
299
+ city:
300
+ type:
301
+ - string
302
+ - 'null'
303
+ state:
304
+ type:
305
+ - string
306
+ - 'null'
307
+ postalCode:
308
+ type:
309
+ - string
310
+ - 'null'
311
+ country:
312
+ type:
313
+ - string
314
+ - 'null'
315
+ notes:
316
+ type:
317
+ - string
318
+ - 'null'
319
+ createdBy:
320
+ type:
321
+ - string
322
+ - 'null'
323
+ required:
324
+ - organizationId
325
+ - name
326
+ InsertFloor:
327
+ type: object
328
+ properties:
329
+ propertyId:
330
+ type: integer
331
+ minimum: -2147483648
332
+ maximum: 2147483647
333
+ organizationId:
334
+ type: string
335
+ name:
336
+ type: string
337
+ level:
338
+ type: integer
339
+ minimum: -2147483648
340
+ maximum: 2147483647
341
+ sortOrder:
342
+ type: integer
343
+ minimum: -2147483648
344
+ maximum: 2147483647
345
+ notes:
346
+ type:
347
+ - string
348
+ - 'null'
349
+ required:
350
+ - propertyId
351
+ - organizationId
352
+ InsertScan:
353
+ type: object
354
+ properties:
355
+ jobId:
356
+ type: integer
357
+ minimum: -2147483648
358
+ maximum: 2147483647
359
+ organizationId:
360
+ type: string
361
+ assetId:
362
+ type:
363
+ - string
364
+ - 'null'
365
+ name:
366
+ type: string
367
+ description:
368
+ type:
369
+ - string
370
+ - 'null'
371
+ fileType:
372
+ type: string
373
+ scaleFactor:
374
+ type:
375
+ - number
376
+ - 'null'
377
+ minimum: -140737488355328
378
+ maximum: 140737488355327
379
+ scaleCalibrated:
380
+ type:
381
+ - boolean
382
+ - 'null'
383
+ pageNumber:
384
+ type:
385
+ - integer
386
+ - 'null'
387
+ minimum: -2147483648
388
+ maximum: 2147483647
389
+ totalPages:
390
+ type:
391
+ - integer
392
+ - 'null'
393
+ minimum: -2147483648
394
+ maximum: 2147483647
395
+ processingStatus:
396
+ type: string
397
+ aiAnalysis:
398
+ anyOf:
399
+ - type: string
400
+ - type: number
401
+ - type: boolean
402
+ - type: 'null'
403
+ - type: object
404
+ additionalProperties: {}
405
+ - type: array
406
+ items: {}
407
+ - type: 'null'
408
+ featureTier:
409
+ type:
410
+ - string
411
+ - 'null'
412
+ xactimateMetadata:
413
+ anyOf:
414
+ - type: string
415
+ - type: number
416
+ - type: boolean
417
+ - type: 'null'
418
+ - type: object
419
+ additionalProperties: {}
420
+ - type: array
421
+ items: {}
422
+ - type: 'null'
423
+ parentScanId:
424
+ type:
425
+ - integer
426
+ - 'null'
427
+ minimum: -2147483648
428
+ maximum: 2147483647
429
+ doorwayLinks:
430
+ anyOf:
431
+ - type: string
432
+ - type: number
433
+ - type: boolean
434
+ - type: 'null'
435
+ - type: object
436
+ additionalProperties: {}
437
+ - type: array
438
+ items: {}
439
+ - type: 'null'
440
+ rawRoomPlanData:
441
+ anyOf:
442
+ - type: string
443
+ - type: number
444
+ - type: boolean
445
+ - type: 'null'
446
+ - type: object
447
+ additionalProperties: {}
448
+ - type: array
449
+ items: {}
450
+ - type: 'null'
451
+ cleanupPipelineVersion:
452
+ type:
453
+ - string
454
+ - 'null'
455
+ clientCleanupPipelineVersion:
456
+ type:
457
+ - string
458
+ - 'null'
459
+ userOrientationHint:
460
+ type:
461
+ - string
462
+ - 'null'
463
+ wallConfidences:
464
+ anyOf:
465
+ - type: string
466
+ - type: number
467
+ - type: boolean
468
+ - type: 'null'
469
+ - type: object
470
+ additionalProperties: {}
471
+ - type: array
472
+ items: {}
473
+ - type: 'null'
474
+ deviceTrack:
475
+ anyOf:
476
+ - type: string
477
+ - type: number
478
+ - type: boolean
479
+ - type: 'null'
480
+ - type: object
481
+ additionalProperties: {}
482
+ - type: array
483
+ items: {}
484
+ - type: 'null'
485
+ referencePhotoKeys:
486
+ type:
487
+ - array
488
+ - 'null'
489
+ items:
490
+ type:
491
+ - string
492
+ - 'null'
493
+ metricCorrection:
494
+ type:
495
+ - number
496
+ - 'null'
497
+ minimum: -140737488355328
498
+ maximum: 140737488355327
499
+ squareUpAngleRad:
500
+ type:
501
+ - number
502
+ - 'null'
503
+ minimum: -140737488355328
504
+ maximum: 140737488355327
505
+ clientMirrorApplied:
506
+ type:
507
+ - boolean
508
+ - 'null'
509
+ deviceModel:
510
+ type:
511
+ - string
512
+ - 'null'
513
+ maxLength: 64
514
+ appVersion:
515
+ type:
516
+ - string
517
+ - 'null'
518
+ maxLength: 64
519
+ captureTimestamp:
520
+ type:
521
+ - string
522
+ - 'null'
523
+ captureMode:
524
+ type:
525
+ - string
526
+ - 'null'
527
+ maxLength: 16
528
+ sessionId:
529
+ type:
530
+ - string
531
+ - 'null'
532
+ maxLength: 64
533
+ sessionCapturedAt:
534
+ type:
535
+ - string
536
+ - 'null'
537
+ floorPlanAssetId:
538
+ type:
539
+ - string
540
+ - 'null'
541
+ floorPlanPixelsPerMeter:
542
+ type:
543
+ - number
544
+ - 'null'
545
+ minimum: -140737488355328
546
+ maximum: 140737488355327
547
+ floorPlanOffsetX:
548
+ type:
549
+ - number
550
+ - 'null'
551
+ minimum: -140737488355328
552
+ maximum: 140737488355327
553
+ floorPlanOffsetY:
554
+ type:
555
+ - number
556
+ - 'null'
557
+ minimum: -140737488355328
558
+ maximum: 140737488355327
559
+ floorPlanWidthPx:
560
+ type:
561
+ - integer
562
+ - 'null'
563
+ minimum: -2147483648
564
+ maximum: 2147483647
565
+ floorPlanHeightPx:
566
+ type:
567
+ - integer
568
+ - 'null'
569
+ minimum: -2147483648
570
+ maximum: 2147483647
571
+ floorId:
572
+ type:
573
+ - integer
574
+ - 'null'
575
+ minimum: -2147483648
576
+ maximum: 2147483647
577
+ createdBy:
578
+ type:
579
+ - string
580
+ - 'null'
581
+ deletedAt:
582
+ type:
583
+ - string
584
+ - 'null'
585
+ deletedBy:
586
+ type:
587
+ - string
588
+ - 'null'
589
+ required:
590
+ - jobId
591
+ - organizationId
592
+ - name
593
+ InsertTask:
594
+ type: object
595
+ properties:
596
+ userId:
597
+ type:
598
+ - string
599
+ - 'null'
600
+ organizationId:
601
+ type:
602
+ - string
603
+ - 'null'
604
+ contactId:
605
+ type:
606
+ - integer
607
+ - 'null'
608
+ minimum: -2147483648
609
+ maximum: 2147483647
610
+ projectId:
611
+ type:
612
+ - integer
613
+ - 'null'
614
+ minimum: -2147483648
615
+ maximum: 2147483647
616
+ agentId:
617
+ type:
618
+ - integer
619
+ - 'null'
620
+ minimum: -2147483648
621
+ maximum: 2147483647
622
+ title:
623
+ type: string
624
+ description:
625
+ type:
626
+ - string
627
+ - 'null'
628
+ taskType:
629
+ type:
630
+ - string
631
+ - 'null'
632
+ location:
633
+ type:
634
+ - string
635
+ - 'null'
636
+ startDate:
637
+ type:
638
+ - string
639
+ - 'null'
640
+ dueDate:
641
+ type:
642
+ - string
643
+ - 'null'
644
+ allDay:
645
+ type:
646
+ - boolean
647
+ - 'null'
648
+ status:
649
+ type:
650
+ - string
651
+ - 'null'
652
+ priority:
653
+ type:
654
+ - string
655
+ - 'null'
656
+ assignedTo:
657
+ anyOf:
658
+ - type: string
659
+ - type: number
660
+ - type: boolean
661
+ - type: 'null'
662
+ - type: object
663
+ additionalProperties: {}
664
+ - type: array
665
+ items: {}
666
+ - type: 'null'
667
+ assignedContactIds:
668
+ anyOf:
669
+ - type: string
670
+ - type: number
671
+ - type: boolean
672
+ - type: 'null'
673
+ - type: object
674
+ additionalProperties: {}
675
+ - type: array
676
+ items: {}
677
+ - type: 'null'
678
+ progress:
679
+ type:
680
+ - integer
681
+ - 'null'
682
+ minimum: -2147483648
683
+ maximum: 2147483647
684
+ checklist:
685
+ anyOf:
686
+ - type: string
687
+ - type: number
688
+ - type: boolean
689
+ - type: 'null'
690
+ - type: object
691
+ additionalProperties: {}
692
+ - type: array
693
+ items: {}
694
+ - type: 'null'
695
+ relatedFiles:
696
+ anyOf:
697
+ - type: string
698
+ - type: number
699
+ - type: boolean
700
+ - type: 'null'
701
+ - type: object
702
+ additionalProperties: {}
703
+ - type: array
704
+ items: {}
705
+ - type: 'null'
706
+ reminder:
707
+ type:
708
+ - string
709
+ - 'null'
710
+ enum:
711
+ - none
712
+ - when_due
713
+ - 15_min
714
+ - 30_min
715
+ - 1_hour
716
+ - 1_day
717
+ reminderNotificationId:
718
+ type:
719
+ - integer
720
+ - 'null'
721
+ minimum: -2147483648
722
+ maximum: 2147483647
723
+ isRecurring:
724
+ type:
725
+ - boolean
726
+ - 'null'
727
+ recurrencePattern:
728
+ type:
729
+ - string
730
+ - 'null'
731
+ recurrenceInterval:
732
+ type:
733
+ - integer
734
+ - 'null'
735
+ minimum: -2147483648
736
+ maximum: 2147483647
737
+ recurrenceEndDate:
738
+ type:
739
+ - string
740
+ - 'null'
741
+ parentTaskId:
742
+ type:
743
+ - integer
744
+ - 'null'
745
+ minimum: -2147483648
746
+ maximum: 2147483647
747
+ recurrenceDate:
748
+ type:
749
+ - string
750
+ - 'null'
751
+ isRecurrenceException:
752
+ type:
753
+ - boolean
754
+ - 'null'
755
+ excludedDates:
756
+ anyOf:
757
+ - type: string
758
+ - type: number
759
+ - type: boolean
760
+ - type: 'null'
761
+ - type: object
762
+ additionalProperties: {}
763
+ - type: array
764
+ items: {}
765
+ - type: 'null'
766
+ displayIndex:
767
+ type:
768
+ - integer
769
+ - 'null'
770
+ minimum: -2147483648
771
+ maximum: 2147483647
772
+ deletedAt:
773
+ type:
774
+ - string
775
+ - 'null'
776
+ deletedBy:
777
+ type:
778
+ - string
779
+ - 'null'
780
+ required:
781
+ - title
782
+ InsertNote:
783
+ type: object
784
+ properties:
785
+ userId:
786
+ type:
787
+ - string
788
+ - 'null'
789
+ organizationId:
790
+ type:
791
+ - string
792
+ - 'null'
793
+ contactId:
794
+ type:
795
+ - integer
796
+ - 'null'
797
+ minimum: -2147483648
798
+ maximum: 2147483647
799
+ content:
800
+ type: string
801
+ userName:
802
+ type:
803
+ - string
804
+ - 'null'
805
+ userEmail:
806
+ type:
807
+ - string
808
+ - 'null'
809
+ deletedAt:
810
+ type:
811
+ - string
812
+ - 'null'
813
+ deletedBy:
814
+ type:
815
+ - string
816
+ - 'null'
817
+ required:
818
+ - content
819
+ InsertFile:
820
+ type: object
821
+ properties:
822
+ organizationId:
823
+ type: string
824
+ folderId:
825
+ type: integer
826
+ minimum: -2147483648
827
+ maximum: 2147483647
828
+ name:
829
+ type: string
830
+ originalName:
831
+ type: string
832
+ mimeType:
833
+ type:
834
+ - string
835
+ - 'null'
836
+ size:
837
+ type:
838
+ - integer
839
+ - 'null'
840
+ minimum: -9007199254740991
841
+ maximum: 9007199254740991
842
+ storageUrl:
843
+ type:
844
+ - string
845
+ - 'null'
846
+ assetId:
847
+ type:
848
+ - string
849
+ - 'null'
850
+ thumbnailUrl:
851
+ type:
852
+ - string
853
+ - 'null'
854
+ metadata:
855
+ anyOf:
856
+ - type: string
857
+ - type: number
858
+ - type: boolean
859
+ - type: 'null'
860
+ - type: object
861
+ additionalProperties: {}
862
+ - type: array
863
+ items: {}
864
+ - type: 'null'
865
+ uploadedBy:
866
+ type:
867
+ - string
868
+ - 'null'
869
+ deletedAt:
870
+ type:
871
+ - string
872
+ - 'null'
873
+ deletedBy:
874
+ type:
875
+ - string
876
+ - 'null'
877
+ required:
878
+ - organizationId
879
+ - folderId
880
+ - name
881
+ - originalName
882
+ InsertFolder:
883
+ type: object
884
+ properties:
885
+ organizationId:
886
+ type: string
887
+ name:
888
+ type: string
889
+ parentId:
890
+ type:
891
+ - integer
892
+ - 'null'
893
+ minimum: -2147483648
894
+ maximum: 2147483647
895
+ isSystemFolder:
896
+ type:
897
+ - boolean
898
+ - 'null'
899
+ folderType:
900
+ type:
901
+ - string
902
+ - 'null'
903
+ createdBy:
904
+ type:
905
+ - string
906
+ - 'null'
907
+ deletedAt:
908
+ type:
909
+ - string
910
+ - 'null'
911
+ deletedBy:
912
+ type:
913
+ - string
914
+ - 'null'
915
+ required:
916
+ - organizationId
917
+ - name
918
+ InsertEstimate:
919
+ type: object
920
+ properties:
921
+ jobId:
922
+ type: integer
923
+ minimum: -2147483648
924
+ maximum: 2147483647
925
+ estimateNumber:
926
+ type: string
927
+ name:
928
+ type:
929
+ - string
930
+ - 'null'
931
+ version:
932
+ type:
933
+ - integer
934
+ - 'null'
935
+ minimum: -2147483648
936
+ maximum: 2147483647
937
+ templateId:
938
+ type:
939
+ - integer
940
+ - 'null'
941
+ minimum: -2147483648
942
+ maximum: 2147483647
943
+ pricelistId:
944
+ type:
945
+ - integer
946
+ - 'null'
947
+ minimum: -2147483648
948
+ maximum: 2147483647
949
+ status:
950
+ type:
951
+ - string
952
+ - 'null'
953
+ subtotal:
954
+ type:
955
+ - string
956
+ - 'null'
957
+ taxAmount:
958
+ type:
959
+ - string
960
+ - 'null'
961
+ discountAmount:
962
+ type:
963
+ - string
964
+ - 'null'
965
+ discountPercent:
966
+ type:
967
+ - string
968
+ - 'null'
969
+ overheadEnabled:
970
+ type:
971
+ - boolean
972
+ - 'null'
973
+ overheadPercent:
974
+ type:
975
+ - string
976
+ - 'null'
977
+ profitEnabled:
978
+ type:
979
+ - boolean
980
+ - 'null'
981
+ profitPercent:
982
+ type:
983
+ - string
984
+ - 'null'
985
+ totalAmount:
986
+ type:
987
+ - string
988
+ - 'null'
989
+ description:
990
+ type:
991
+ - string
992
+ - 'null'
993
+ sections:
994
+ anyOf:
995
+ - type: string
996
+ - type: number
997
+ - type: boolean
998
+ - type: 'null'
999
+ - type: object
1000
+ additionalProperties: {}
1001
+ - type: array
1002
+ items: {}
1003
+ - type: 'null'
1004
+ terms:
1005
+ type:
1006
+ - string
1007
+ - 'null'
1008
+ notes:
1009
+ type:
1010
+ - string
1011
+ - 'null'
1012
+ internalNotes:
1013
+ type:
1014
+ - string
1015
+ - 'null'
1016
+ validUntil:
1017
+ type:
1018
+ - string
1019
+ - 'null'
1020
+ sentAt:
1021
+ type:
1022
+ - string
1023
+ - 'null'
1024
+ approvedAt:
1025
+ type:
1026
+ - string
1027
+ - 'null'
1028
+ approvedBy:
1029
+ type:
1030
+ - string
1031
+ - 'null'
1032
+ rejectedAt:
1033
+ type:
1034
+ - string
1035
+ - 'null'
1036
+ rejectionReason:
1037
+ type:
1038
+ - string
1039
+ - 'null'
1040
+ signedAt:
1041
+ type:
1042
+ - string
1043
+ - 'null'
1044
+ signatureMeta:
1045
+ anyOf:
1046
+ - type: string
1047
+ - type: number
1048
+ - type: boolean
1049
+ - type: 'null'
1050
+ - type: object
1051
+ additionalProperties: {}
1052
+ - type: array
1053
+ items: {}
1054
+ - type: 'null'
1055
+ signatureProvider:
1056
+ type:
1057
+ - string
1058
+ - 'null'
1059
+ signatureId:
1060
+ type:
1061
+ - string
1062
+ - 'null'
1063
+ signerName:
1064
+ type:
1065
+ - string
1066
+ - 'null'
1067
+ signerEmail:
1068
+ type:
1069
+ - string
1070
+ - 'null'
1071
+ signerIp:
1072
+ type:
1073
+ - string
1074
+ - 'null'
1075
+ revisionRequestedAt:
1076
+ type:
1077
+ - string
1078
+ - 'null'
1079
+ revisionNotes:
1080
+ type:
1081
+ - string
1082
+ - 'null'
1083
+ customerResponseAt:
1084
+ type:
1085
+ - string
1086
+ - 'null'
1087
+ convertedToInvoiceId:
1088
+ type:
1089
+ - integer
1090
+ - 'null'
1091
+ minimum: -2147483648
1092
+ maximum: 2147483647
1093
+ voidedAt:
1094
+ type:
1095
+ - string
1096
+ - 'null'
1097
+ voidedBy:
1098
+ type:
1099
+ - string
1100
+ - 'null'
1101
+ voidedReason:
1102
+ type:
1103
+ - string
1104
+ - 'null'
1105
+ signatureLocked:
1106
+ type:
1107
+ - boolean
1108
+ - 'null'
1109
+ signNowStatus:
1110
+ type:
1111
+ - string
1112
+ - 'null'
1113
+ signNowRoleId:
1114
+ type:
1115
+ - string
1116
+ - 'null'
1117
+ signNowRoleName:
1118
+ type:
1119
+ - string
1120
+ - 'null'
1121
+ portalVisible:
1122
+ type:
1123
+ - boolean
1124
+ - 'null'
1125
+ signedCompletionEmailSentAt:
1126
+ type:
1127
+ - string
1128
+ - 'null'
1129
+ qboId:
1130
+ type:
1131
+ - string
1132
+ - 'null'
1133
+ qboSyncToken:
1134
+ type:
1135
+ - string
1136
+ - 'null'
1137
+ qboLastPushedAt:
1138
+ type:
1139
+ - string
1140
+ - 'null'
1141
+ qboLastPushedHash:
1142
+ type:
1143
+ - string
1144
+ - 'null'
1145
+ qboLastError:
1146
+ type:
1147
+ - string
1148
+ - 'null'
1149
+ createdBy:
1150
+ type:
1151
+ - string
1152
+ - 'null'
1153
+ deletedAt:
1154
+ type:
1155
+ - string
1156
+ - 'null'
1157
+ deletedBy:
1158
+ type:
1159
+ - string
1160
+ - 'null'
1161
+ required:
1162
+ - jobId
1163
+ - estimateNumber
1164
+ PhotoUploadResponse:
1165
+ type: object
1166
+ properties:
1167
+ id:
1168
+ type: integer
1169
+ url:
1170
+ type: string
1171
+ s3Key:
1172
+ type: string
1173
+ contentType:
1174
+ type: string
1175
+ width:
1176
+ type:
1177
+ - integer
1178
+ - 'null'
1179
+ height:
1180
+ type:
1181
+ - integer
1182
+ - 'null'
1183
+ watermarked:
1184
+ type: boolean
1185
+ required:
1186
+ - id
1187
+ - url
1188
+ - s3Key
1189
+ - contentType
1190
+ - watermarked
1191
+ MeResponse:
1192
+ type: object
1193
+ properties:
1194
+ id:
1195
+ type: integer
1196
+ email:
1197
+ type: string
1198
+ format: email
1199
+ name:
1200
+ type:
1201
+ - string
1202
+ - 'null'
1203
+ orgId:
1204
+ type: integer
1205
+ role:
1206
+ type: string
1207
+ required:
1208
+ - id
1209
+ - email
1210
+ - orgId
1211
+ - role
1212
+ MobileUploadRequest:
1213
+ type: object
1214
+ properties:
1215
+ roomPlanJSON:
1216
+ type: object
1217
+ properties:
1218
+ rooms:
1219
+ type: array
1220
+ items:
1221
+ type: object
1222
+ properties:
1223
+ label:
1224
+ type: string
1225
+ name:
1226
+ type: string
1227
+ category:
1228
+ type: string
1229
+ walls:
1230
+ type: array
1231
+ items: {}
1232
+ doors:
1233
+ type: array
1234
+ items:
1235
+ type: object
1236
+ properties:
1237
+ position:
1238
+ type: object
1239
+ properties:
1240
+ x:
1241
+ type: number
1242
+ 'y':
1243
+ type: number
1244
+ z:
1245
+ type: number
1246
+ required:
1247
+ - x
1248
+ - 'y'
1249
+ - z
1250
+ width:
1251
+ type: number
1252
+ exclusiveMinimum: 0
1253
+ height:
1254
+ type: number
1255
+ exclusiveMinimum: 0
1256
+ hostWallIndex:
1257
+ type:
1258
+ - integer
1259
+ - 'null'
1260
+ minimum: 0
1261
+ wallIndex:
1262
+ type: integer
1263
+ minimum: 0
1264
+ swingHand:
1265
+ type: string
1266
+ enum: &ref_0
1267
+ - left
1268
+ - right
1269
+ - none
1270
+ required:
1271
+ - position
1272
+ - width
1273
+ - height
1274
+ windows:
1275
+ type: array
1276
+ items:
1277
+ type: object
1278
+ properties:
1279
+ position:
1280
+ type: object
1281
+ properties:
1282
+ x:
1283
+ type: number
1284
+ 'y':
1285
+ type: number
1286
+ z:
1287
+ type: number
1288
+ required:
1289
+ - x
1290
+ - 'y'
1291
+ - z
1292
+ width:
1293
+ type: number
1294
+ exclusiveMinimum: 0
1295
+ height:
1296
+ type: number
1297
+ exclusiveMinimum: 0
1298
+ hostWallIndex:
1299
+ type:
1300
+ - integer
1301
+ - 'null'
1302
+ minimum: 0
1303
+ wallIndex:
1304
+ type: integer
1305
+ minimum: 0
1306
+ swingHand:
1307
+ type: string
1308
+ enum: *ref_0
1309
+ required:
1310
+ - position
1311
+ - width
1312
+ - height
1313
+ openings:
1314
+ type: array
1315
+ items:
1316
+ type: object
1317
+ properties:
1318
+ position:
1319
+ type: object
1320
+ properties:
1321
+ x:
1322
+ type: number
1323
+ 'y':
1324
+ type: number
1325
+ z:
1326
+ type: number
1327
+ required:
1328
+ - x
1329
+ - 'y'
1330
+ - z
1331
+ width:
1332
+ type: number
1333
+ exclusiveMinimum: 0
1334
+ height:
1335
+ type: number
1336
+ exclusiveMinimum: 0
1337
+ hostWallIndex:
1338
+ type:
1339
+ - integer
1340
+ - 'null'
1341
+ minimum: 0
1342
+ wallIndex:
1343
+ type: integer
1344
+ minimum: 0
1345
+ swingHand:
1346
+ type: string
1347
+ enum: *ref_0
1348
+ required:
1349
+ - position
1350
+ - width
1351
+ - height
1352
+ required:
1353
+ - rooms
1354
+ name:
1355
+ type: string
1356
+ minLength: 1
1357
+ description:
1358
+ type: string
1359
+ assetId:
1360
+ type: string
1361
+ autoMeasure:
1362
+ type: boolean
1363
+ cleanupPipelineVersion:
1364
+ type: string
1365
+ userOrientationHint:
1366
+ type: string
1367
+ wallConfidences:
1368
+ type: array
1369
+ items: {}
1370
+ deviceTrack: {}
1371
+ referencePhotos:
1372
+ type: array
1373
+ items: {}
1374
+ propertyId:
1375
+ type: integer
1376
+ exclusiveMinimum: 0
1377
+ floorId:
1378
+ type: integer
1379
+ exclusiveMinimum: 0
1380
+ floorName:
1381
+ type: string
1382
+ minLength: 1
1383
+ maxLength: 128
1384
+ floorLevel:
1385
+ type: integer
1386
+ roomLocalId:
1387
+ type: string
1388
+ maxLength: 128
1389
+ roomNotes:
1390
+ type: array
1391
+ items:
1392
+ type: object
1393
+ properties:
1394
+ roomLocalId:
1395
+ type: string
1396
+ note:
1397
+ type: string
1398
+ maxLength: 4096
1399
+ required:
1400
+ - roomLocalId
1401
+ - note
1402
+ deviceModel:
1403
+ type: string
1404
+ maxLength: 64
1405
+ appVersion:
1406
+ type: string
1407
+ maxLength: 64
1408
+ captureTimestamp:
1409
+ type: string
1410
+ manualMeasurements:
1411
+ type: array
1412
+ items:
1413
+ type: object
1414
+ properties:
1415
+ id:
1416
+ type: string
1417
+ roomLocalId:
1418
+ type: string
1419
+ wallIndex:
1420
+ type: integer
1421
+ minimum: 0
1422
+ valueMeters:
1423
+ type: number
1424
+ exclusiveMinimum: 0
1425
+ maximum: 100
1426
+ capturedAt:
1427
+ type: string
1428
+ capturedBy:
1429
+ type:
1430
+ - string
1431
+ - 'null'
1432
+ required:
1433
+ - id
1434
+ - roomLocalId
1435
+ - wallIndex
1436
+ - valueMeters
1437
+ - capturedAt
1438
+ captureMode:
1439
+ type: string
1440
+ enum:
1441
+ - session
1442
+ - view
1443
+ sessionId:
1444
+ type: string
1445
+ sessionCapturedAt:
1446
+ type: string
1447
+ clientMirrorApplied:
1448
+ type: boolean
1449
+ required:
1450
+ - roomPlanJSON
1451
+ - name
1452
+ description: >-
1453
+ POST /api/jobs/:jobId/scans/mobile-upload body. Required: `roomPlanJSON` (object with nested `rooms` array) +
1454
+ `name`. All other fields are additive / optional per the v2 contract.
1455
+ MobileUploadResponse:
1456
+ type: object
1457
+ properties:
1458
+ scanId:
1459
+ type: integer
1460
+ processingStatus:
1461
+ type: string
1462
+ floorId:
1463
+ type:
1464
+ - integer
1465
+ - 'null'
1466
+ manualMeasurementCount:
1467
+ type: integer
1468
+ message:
1469
+ type: string
1470
+ parentScanId:
1471
+ type: integer
1472
+ appendRoom:
1473
+ type: boolean
1474
+ enum:
1475
+ - true
1476
+ idempotent:
1477
+ type: boolean
1478
+ required:
1479
+ - scanId
1480
+ - processingStatus
1481
+ - floorId
1482
+ - manualMeasurementCount
1483
+ - message
1484
+ FeatureFlags:
1485
+ type: object
1486
+ properties:
1487
+ floorPlanPivot:
1488
+ type: boolean
1489
+ manualMeasurements:
1490
+ type: boolean
1491
+ scanRoomSnapshots:
1492
+ type: boolean
1493
+ roomConnections:
1494
+ type: boolean
1495
+ required:
1496
+ - floorPlanPivot
1497
+ - manualMeasurements
1498
+ - scanRoomSnapshots
1499
+ - roomConnections
1500
+ JobSummary:
1501
+ type: object
1502
+ properties:
1503
+ id:
1504
+ type: integer
1505
+ name:
1506
+ type: string
1507
+ description:
1508
+ type:
1509
+ - string
1510
+ - 'null'
1511
+ status:
1512
+ type: string
1513
+ propertyId:
1514
+ type:
1515
+ - integer
1516
+ - 'null'
1517
+ contactId:
1518
+ type:
1519
+ - integer
1520
+ - 'null'
1521
+ workflowId:
1522
+ type:
1523
+ - integer
1524
+ - 'null'
1525
+ workflowStage:
1526
+ type:
1527
+ - string
1528
+ - 'null'
1529
+ startDate:
1530
+ type:
1531
+ - string
1532
+ - 'null'
1533
+ dueDate:
1534
+ type:
1535
+ - string
1536
+ - 'null'
1537
+ createdAt:
1538
+ type: string
1539
+ required:
1540
+ - id
1541
+ - name
1542
+ - status
1543
+ - createdAt
1544
+ JobCreateRequest:
1545
+ type: object
1546
+ properties:
1547
+ name:
1548
+ type: string
1549
+ minLength: 1
1550
+ description:
1551
+ type: string
1552
+ propertyId:
1553
+ type: integer
1554
+ exclusiveMinimum: 0
1555
+ contactId:
1556
+ type: integer
1557
+ exclusiveMinimum: 0
1558
+ workflowId:
1559
+ type: integer
1560
+ exclusiveMinimum: 0
1561
+ startDate:
1562
+ type: string
1563
+ dueDate:
1564
+ type: string
1565
+ required:
1566
+ - name
1567
+ ChattySessionStartRequest:
1568
+ type: object
1569
+ properties:
1570
+ jobId:
1571
+ type: integer
1572
+ exclusiveMinimum: 0
1573
+ contactId:
1574
+ type: integer
1575
+ exclusiveMinimum: 0
1576
+ locale:
1577
+ type: string
1578
+ example: en-US
1579
+ voice:
1580
+ type: string
1581
+ example: alloy
1582
+ systemPromptOverride:
1583
+ type: string
1584
+ ChattySessionToken:
1585
+ type: object
1586
+ properties:
1587
+ sessionId:
1588
+ type: string
1589
+ expiresAt:
1590
+ type: string
1591
+ websocketUrl:
1592
+ type: string
1593
+ ephemeralToken:
1594
+ type: string
1595
+ model:
1596
+ type: string
1597
+ voice:
1598
+ type: string
1599
+ required:
1600
+ - sessionId
1601
+ - expiresAt
1602
+ - websocketUrl
1603
+ - ephemeralToken
1604
+ - model
1605
+ - voice
1606
+ ChattyMessage:
1607
+ type: object
1608
+ properties:
1609
+ id:
1610
+ type: string
1611
+ role:
1612
+ type: string
1613
+ enum:
1614
+ - user
1615
+ - assistant
1616
+ - tool
1617
+ text:
1618
+ type:
1619
+ - string
1620
+ - 'null'
1621
+ audioUrl:
1622
+ type:
1623
+ - string
1624
+ - 'null'
1625
+ createdAt:
1626
+ type: string
1627
+ toolCalls:
1628
+ type: array
1629
+ items:
1630
+ type: object
1631
+ properties:
1632
+ name:
1633
+ type: string
1634
+ arguments: {}
1635
+ required:
1636
+ - name
1637
+ required:
1638
+ - id
1639
+ - role
1640
+ - createdAt
1641
+ parameters: {}
1642
+ paths:
1643
+ /api/version:
1644
+ get:
1645
+ summary: Cross-platform version handshake manifest.
1646
+ tags:
1647
+ - version
1648
+ responses:
1649
+ '200':
1650
+ description: Always returned. Body is the canonical handshake manifest.
1651
+ content:
1652
+ application/json:
1653
+ schema:
1654
+ $ref: '#/components/schemas/VersionManifest'
1655
+ /api/ping:
1656
+ get:
1657
+ summary: Liveness probe. Bypasses the version handshake.
1658
+ tags:
1659
+ - version
1660
+ responses:
1661
+ '200':
1662
+ description: Static OK envelope.
1663
+ content:
1664
+ application/json:
1665
+ schema:
1666
+ type: object
1667
+ properties:
1668
+ status:
1669
+ type: string
1670
+ enum:
1671
+ - ok
1672
+ timestamp:
1673
+ type: string
1674
+ env:
1675
+ type:
1676
+ - string
1677
+ - 'null'
1678
+ required:
1679
+ - status
1680
+ - timestamp
1681
+ - env
1682
+ /api/me/features:
1683
+ get:
1684
+ summary: Capability discovery — iOS calls on launch to gate UI affordances.
1685
+ tags:
1686
+ - auth
1687
+ - features
1688
+ parameters:
1689
+ - &ref_1
1690
+ name: X-Client-Version
1691
+ in: header
1692
+ required: false
1693
+ schema:
1694
+ type: string
1695
+ example: 1.0.0
1696
+ description: Semver of the calling app build. Drives the 426 / soft-upgrade handshake.
1697
+ responses:
1698
+ '200':
1699
+ description: Flat feature map. Unknown / missing flags must be treated as false by the client.
1700
+ content:
1701
+ application/json:
1702
+ schema:
1703
+ $ref: '#/components/schemas/FeatureFlags'
1704
+ '426':
1705
+ description: Client major below minClientVersion.
1706
+ content:
1707
+ application/json:
1708
+ schema:
1709
+ $ref: '#/components/schemas/UpgradeRequiredEnvelope'
1710
+ /api/jobs/{jobId}/scans/mobile-upload:
1711
+ post:
1712
+ summary: iOS LiDAR scan upload. v2 fields are additive — v1 payloads remain byte-identical.
1713
+ tags:
1714
+ - lidar
1715
+ parameters:
1716
+ - *ref_1
1717
+ - schema:
1718
+ type: integer
1719
+ exclusiveMinimum: 0
1720
+ required: true
1721
+ name: jobId
1722
+ in: path
1723
+ requestBody:
1724
+ content:
1725
+ application/json:
1726
+ schema:
1727
+ $ref: '#/components/schemas/MobileUploadRequest'
1728
+ responses:
1729
+ '200':
1730
+ description: Idempotent replay of a session-mode upload (same sessionId).
1731
+ content:
1732
+ application/json:
1733
+ schema:
1734
+ $ref: '#/components/schemas/MobileUploadResponse'
1735
+ '201':
1736
+ description: Scan created (or composite child appended via parentScanId+entryDoorwayId).
1737
+ content:
1738
+ application/json:
1739
+ schema:
1740
+ $ref: '#/components/schemas/MobileUploadResponse'
1741
+ '400':
1742
+ description: Validation failure.
1743
+ content:
1744
+ application/json:
1745
+ schema:
1746
+ $ref: '#/components/schemas/ErrorEnvelope'
1747
+ '403':
1748
+ description: Caller not assigned to this job.
1749
+ content:
1750
+ application/json:
1751
+ schema:
1752
+ $ref: '#/components/schemas/ErrorEnvelope'
1753
+ '404':
1754
+ description: Job not found.
1755
+ content:
1756
+ application/json:
1757
+ schema:
1758
+ $ref: '#/components/schemas/ErrorEnvelope'
1759
+ '426':
1760
+ description: Client major below minClientVersion.
1761
+ content:
1762
+ application/json:
1763
+ schema:
1764
+ $ref: '#/components/schemas/UpgradeRequiredEnvelope'
1765
+ /api/jobs/{jobId}/floors:
1766
+ post:
1767
+ summary: Idempotent floor mint. Lookup is case-/whitespace-insensitive on name.
1768
+ tags:
1769
+ - lidar
1770
+ parameters:
1771
+ - *ref_1
1772
+ - schema:
1773
+ type: integer
1774
+ exclusiveMinimum: 0
1775
+ required: true
1776
+ name: jobId
1777
+ in: path
1778
+ requestBody:
1779
+ content:
1780
+ application/json:
1781
+ schema:
1782
+ type: object
1783
+ properties:
1784
+ name:
1785
+ type: string
1786
+ minLength: 1
1787
+ maxLength: 128
1788
+ level:
1789
+ type: integer
1790
+ sortOrder:
1791
+ type: integer
1792
+ notes:
1793
+ type: string
1794
+ propertyId:
1795
+ type: integer
1796
+ exclusiveMinimum: 0
1797
+ required:
1798
+ - name
1799
+ responses:
1800
+ '200':
1801
+ description: Existing floor matched by normalized name.
1802
+ content:
1803
+ application/json:
1804
+ schema:
1805
+ type: object
1806
+ properties:
1807
+ created:
1808
+ type: boolean
1809
+ enum:
1810
+ - false
1811
+ floor:
1812
+ type: object
1813
+ properties:
1814
+ propertyId:
1815
+ type: integer
1816
+ minimum: -2147483648
1817
+ maximum: 2147483647
1818
+ organizationId:
1819
+ type: string
1820
+ name:
1821
+ type: string
1822
+ level:
1823
+ type: integer
1824
+ minimum: -2147483648
1825
+ maximum: 2147483647
1826
+ sortOrder:
1827
+ type: integer
1828
+ minimum: -2147483648
1829
+ maximum: 2147483647
1830
+ notes:
1831
+ type:
1832
+ - string
1833
+ - 'null'
1834
+ id:
1835
+ type: number
1836
+ required:
1837
+ - propertyId
1838
+ - organizationId
1839
+ - id
1840
+ required:
1841
+ - created
1842
+ - floor
1843
+ '201':
1844
+ description: Fresh floor inserted.
1845
+ content:
1846
+ application/json:
1847
+ schema:
1848
+ type: object
1849
+ properties:
1850
+ created:
1851
+ type: boolean
1852
+ enum:
1853
+ - true
1854
+ floor:
1855
+ type: object
1856
+ properties:
1857
+ propertyId:
1858
+ type: integer
1859
+ minimum: -2147483648
1860
+ maximum: 2147483647
1861
+ organizationId:
1862
+ type: string
1863
+ name:
1864
+ type: string
1865
+ level:
1866
+ type: integer
1867
+ minimum: -2147483648
1868
+ maximum: 2147483647
1869
+ sortOrder:
1870
+ type: integer
1871
+ minimum: -2147483648
1872
+ maximum: 2147483647
1873
+ notes:
1874
+ type:
1875
+ - string
1876
+ - 'null'
1877
+ id:
1878
+ type: number
1879
+ required:
1880
+ - propertyId
1881
+ - organizationId
1882
+ - id
1883
+ required:
1884
+ - created
1885
+ - floor
1886
+ '400':
1887
+ description: Validation failure.
1888
+ content:
1889
+ application/json:
1890
+ schema:
1891
+ $ref: '#/components/schemas/ErrorEnvelope'
1892
+ '403':
1893
+ description: Caller cannot reach this job.
1894
+ content:
1895
+ application/json:
1896
+ schema:
1897
+ $ref: '#/components/schemas/ErrorEnvelope'
1898
+ /api/jobs:
1899
+ get:
1900
+ summary: List jobs (projects) visible to the caller.
1901
+ tags:
1902
+ - jobs
1903
+ parameters:
1904
+ - *ref_1
1905
+ responses:
1906
+ '200':
1907
+ description: Array of jobs.
1908
+ content:
1909
+ application/json:
1910
+ schema:
1911
+ type: array
1912
+ items:
1913
+ $ref: '#/components/schemas/JobSummary'
1914
+ '401':
1915
+ description: Unauthenticated.
1916
+ content:
1917
+ application/json:
1918
+ schema:
1919
+ $ref: '#/components/schemas/ErrorEnvelope'
1920
+ post:
1921
+ summary: Create a job.
1922
+ tags:
1923
+ - jobs
1924
+ parameters:
1925
+ - *ref_1
1926
+ requestBody:
1927
+ content:
1928
+ application/json:
1929
+ schema:
1930
+ $ref: '#/components/schemas/JobCreateRequest'
1931
+ responses:
1932
+ '201':
1933
+ description: Job created.
1934
+ content:
1935
+ application/json:
1936
+ schema:
1937
+ $ref: '#/components/schemas/JobSummary'
1938
+ '400':
1939
+ description: Validation failure.
1940
+ content:
1941
+ application/json:
1942
+ schema:
1943
+ $ref: '#/components/schemas/ErrorEnvelope'
1944
+ /api/jobs/{jobId}:
1945
+ get:
1946
+ summary: Fetch a single job by id.
1947
+ tags:
1948
+ - jobs
1949
+ parameters:
1950
+ - *ref_1
1951
+ - schema:
1952
+ type: integer
1953
+ exclusiveMinimum: 0
1954
+ required: true
1955
+ name: jobId
1956
+ in: path
1957
+ responses:
1958
+ '200':
1959
+ description: Job detail.
1960
+ content:
1961
+ application/json:
1962
+ schema:
1963
+ $ref: '#/components/schemas/JobSummary'
1964
+ '403':
1965
+ description: Caller cannot reach this job.
1966
+ content:
1967
+ application/json:
1968
+ schema:
1969
+ $ref: '#/components/schemas/ErrorEnvelope'
1970
+ '404':
1971
+ description: Job not found.
1972
+ content:
1973
+ application/json:
1974
+ schema:
1975
+ $ref: '#/components/schemas/ErrorEnvelope'
1976
+ /api/chatty/sessions:
1977
+ post:
1978
+ summary: Mint an ephemeral realtime voice session token.
1979
+ tags:
1980
+ - chatty
1981
+ parameters:
1982
+ - *ref_1
1983
+ requestBody:
1984
+ content:
1985
+ application/json:
1986
+ schema:
1987
+ $ref: '#/components/schemas/ChattySessionStartRequest'
1988
+ responses:
1989
+ '201':
1990
+ description: Session created. Connect to websocketUrl with ephemeralToken in the Authorization header.
1991
+ content:
1992
+ application/json:
1993
+ schema:
1994
+ $ref: '#/components/schemas/ChattySessionToken'
1995
+ '400':
1996
+ description: Validation failure.
1997
+ content:
1998
+ application/json:
1999
+ schema:
2000
+ $ref: '#/components/schemas/ErrorEnvelope'
2001
+ '401':
2002
+ description: Unauthenticated.
2003
+ content:
2004
+ application/json:
2005
+ schema:
2006
+ $ref: '#/components/schemas/ErrorEnvelope'
2007
+ '426':
2008
+ description: Client major below minClientVersion.
2009
+ content:
2010
+ application/json:
2011
+ schema:
2012
+ $ref: '#/components/schemas/UpgradeRequiredEnvelope'
2013
+ /api/chatty/sessions/{sessionId}/messages:
2014
+ get:
2015
+ summary: List transcript messages for a Chatty session.
2016
+ tags:
2017
+ - chatty
2018
+ parameters:
2019
+ - *ref_1
2020
+ - schema:
2021
+ type: string
2022
+ required: true
2023
+ name: sessionId
2024
+ in: path
2025
+ responses:
2026
+ '200':
2027
+ description: Ordered transcript.
2028
+ content:
2029
+ application/json:
2030
+ schema:
2031
+ type: array
2032
+ items:
2033
+ $ref: '#/components/schemas/ChattyMessage'
2034
+ '403':
2035
+ description: Session does not belong to caller.
2036
+ content:
2037
+ application/json:
2038
+ schema:
2039
+ $ref: '#/components/schemas/ErrorEnvelope'
2040
+ '404':
2041
+ description: Session not found.
2042
+ content:
2043
+ application/json:
2044
+ schema:
2045
+ $ref: '#/components/schemas/ErrorEnvelope'
2046
+ /api/chatty/sessions/{sessionId}:
2047
+ delete:
2048
+ summary: End a Chatty session and revoke its ephemeral token.
2049
+ tags:
2050
+ - chatty
2051
+ parameters:
2052
+ - *ref_1
2053
+ - schema:
2054
+ type: string
2055
+ required: true
2056
+ name: sessionId
2057
+ in: path
2058
+ responses:
2059
+ '204':
2060
+ description: Session terminated.
2061
+ '403':
2062
+ description: Session does not belong to caller.
2063
+ content:
2064
+ application/json:
2065
+ schema:
2066
+ $ref: '#/components/schemas/ErrorEnvelope'
2067
+ '404':
2068
+ description: Session not found.
2069
+ content:
2070
+ application/json:
2071
+ schema:
2072
+ $ref: '#/components/schemas/ErrorEnvelope'
2073
+ /api/me:
2074
+ get:
2075
+ summary: Current session principal. iOS calls this immediately after /api/version on launch.
2076
+ tags:
2077
+ - auth
2078
+ parameters:
2079
+ - *ref_1
2080
+ responses:
2081
+ '200':
2082
+ description: Authenticated user.
2083
+ content:
2084
+ application/json:
2085
+ schema:
2086
+ $ref: '#/components/schemas/MeResponse'
2087
+ '401':
2088
+ description: No active session.
2089
+ content:
2090
+ application/json:
2091
+ schema:
2092
+ $ref: '#/components/schemas/ErrorEnvelope'
2093
+ '426':
2094
+ description: Client major below minClientVersion.
2095
+ content:
2096
+ application/json:
2097
+ schema:
2098
+ $ref: '#/components/schemas/UpgradeRequiredEnvelope'
2099
+ /api/contacts:
2100
+ get:
2101
+ summary: List contacts in the caller's org.
2102
+ tags:
2103
+ - crm
2104
+ parameters:
2105
+ - *ref_1
2106
+ responses:
2107
+ '200':
2108
+ description: Array of contacts.
2109
+ content:
2110
+ application/json:
2111
+ schema:
2112
+ type: array
2113
+ items:
2114
+ allOf:
2115
+ - type: object
2116
+ properties:
2117
+ userId:
2118
+ type:
2119
+ - string
2120
+ - 'null'
2121
+ organizationId:
2122
+ type:
2123
+ - string
2124
+ - 'null'
2125
+ customerNumber:
2126
+ type:
2127
+ - integer
2128
+ - 'null'
2129
+ minimum: -2147483648
2130
+ maximum: 2147483647
2131
+ name:
2132
+ type: string
2133
+ email:
2134
+ type:
2135
+ - string
2136
+ - 'null'
2137
+ phone:
2138
+ type:
2139
+ - string
2140
+ - 'null'
2141
+ company:
2142
+ type:
2143
+ - string
2144
+ - 'null'
2145
+ address:
2146
+ type:
2147
+ - string
2148
+ - 'null'
2149
+ status:
2150
+ type:
2151
+ - string
2152
+ - 'null'
2153
+ customFields:
2154
+ anyOf:
2155
+ - type: string
2156
+ - type: number
2157
+ - type: boolean
2158
+ - type: 'null'
2159
+ - type: object
2160
+ additionalProperties: {}
2161
+ - type: array
2162
+ items: {}
2163
+ - type: 'null'
2164
+ lastContactDate:
2165
+ type:
2166
+ - string
2167
+ - 'null'
2168
+ parentContactId:
2169
+ type:
2170
+ - integer
2171
+ - 'null'
2172
+ minimum: -2147483648
2173
+ maximum: 2147483647
2174
+ relationship:
2175
+ type:
2176
+ - string
2177
+ - 'null'
2178
+ role:
2179
+ type:
2180
+ - string
2181
+ - 'null'
2182
+ firstName:
2183
+ type:
2184
+ - string
2185
+ - 'null'
2186
+ lastName:
2187
+ type:
2188
+ - string
2189
+ - 'null'
2190
+ serviceAddress:
2191
+ type:
2192
+ - string
2193
+ - 'null'
2194
+ billingAddress:
2195
+ type:
2196
+ - string
2197
+ - 'null'
2198
+ isCommercial:
2199
+ type:
2200
+ - boolean
2201
+ - 'null'
2202
+ pointOfContactName:
2203
+ type:
2204
+ - string
2205
+ - 'null'
2206
+ pointOfContactTitle:
2207
+ type:
2208
+ - string
2209
+ - 'null'
2210
+ pointOfContactPhone:
2211
+ type:
2212
+ - string
2213
+ - 'null'
2214
+ primaryEmail:
2215
+ type:
2216
+ - string
2217
+ - 'null'
2218
+ propertyAddress:
2219
+ type:
2220
+ - string
2221
+ - 'null'
2222
+ leadSource:
2223
+ type:
2224
+ - string
2225
+ - 'null'
2226
+ leadSourceReferralId:
2227
+ type:
2228
+ - integer
2229
+ - 'null'
2230
+ minimum: -2147483648
2231
+ maximum: 2147483647
2232
+ leadSourceReferralUserId:
2233
+ type:
2234
+ - string
2235
+ - 'null'
2236
+ leadSourceOther:
2237
+ type:
2238
+ - string
2239
+ - 'null'
2240
+ googleDriveCustomerFolderId:
2241
+ type:
2242
+ - string
2243
+ - 'null'
2244
+ cachedLat:
2245
+ type:
2246
+ - number
2247
+ - 'null'
2248
+ minimum: -140737488355328
2249
+ maximum: 140737488355327
2250
+ cachedLng:
2251
+ type:
2252
+ - number
2253
+ - 'null'
2254
+ minimum: -140737488355328
2255
+ maximum: 140737488355327
2256
+ qboId:
2257
+ type:
2258
+ - string
2259
+ - 'null'
2260
+ qboSyncToken:
2261
+ type:
2262
+ - string
2263
+ - 'null'
2264
+ qboLastPushedAt:
2265
+ type:
2266
+ - string
2267
+ - 'null'
2268
+ qboLastPushedHash:
2269
+ type:
2270
+ - string
2271
+ - 'null'
2272
+ qboLastError:
2273
+ type:
2274
+ - string
2275
+ - 'null'
2276
+ deletedAt:
2277
+ type:
2278
+ - string
2279
+ - 'null'
2280
+ deletedBy:
2281
+ type:
2282
+ - string
2283
+ - 'null'
2284
+ required:
2285
+ - name
2286
+ - type: object
2287
+ properties:
2288
+ id:
2289
+ type: number
2290
+ required:
2291
+ - id
2292
+ '401':
2293
+ description: Unauthenticated.
2294
+ content:
2295
+ application/json:
2296
+ schema:
2297
+ $ref: '#/components/schemas/ErrorEnvelope'
2298
+ post:
2299
+ summary: Create a contact.
2300
+ tags:
2301
+ - crm
2302
+ parameters:
2303
+ - *ref_1
2304
+ requestBody:
2305
+ content:
2306
+ application/json:
2307
+ schema:
2308
+ type: object
2309
+ properties:
2310
+ userId:
2311
+ type:
2312
+ - string
2313
+ - 'null'
2314
+ organizationId:
2315
+ type:
2316
+ - string
2317
+ - 'null'
2318
+ customerNumber:
2319
+ type:
2320
+ - integer
2321
+ - 'null'
2322
+ minimum: -2147483648
2323
+ maximum: 2147483647
2324
+ name:
2325
+ type: string
2326
+ email:
2327
+ type:
2328
+ - string
2329
+ - 'null'
2330
+ phone:
2331
+ type:
2332
+ - string
2333
+ - 'null'
2334
+ company:
2335
+ type:
2336
+ - string
2337
+ - 'null'
2338
+ address:
2339
+ type:
2340
+ - string
2341
+ - 'null'
2342
+ status:
2343
+ type:
2344
+ - string
2345
+ - 'null'
2346
+ customFields:
2347
+ anyOf:
2348
+ - type: string
2349
+ - type: number
2350
+ - type: boolean
2351
+ - type: 'null'
2352
+ - type: object
2353
+ additionalProperties: {}
2354
+ - type: array
2355
+ items: {}
2356
+ - type: 'null'
2357
+ lastContactDate:
2358
+ type:
2359
+ - string
2360
+ - 'null'
2361
+ parentContactId:
2362
+ type:
2363
+ - integer
2364
+ - 'null'
2365
+ minimum: -2147483648
2366
+ maximum: 2147483647
2367
+ relationship:
2368
+ type:
2369
+ - string
2370
+ - 'null'
2371
+ role:
2372
+ type:
2373
+ - string
2374
+ - 'null'
2375
+ firstName:
2376
+ type:
2377
+ - string
2378
+ - 'null'
2379
+ lastName:
2380
+ type:
2381
+ - string
2382
+ - 'null'
2383
+ serviceAddress:
2384
+ type:
2385
+ - string
2386
+ - 'null'
2387
+ billingAddress:
2388
+ type:
2389
+ - string
2390
+ - 'null'
2391
+ isCommercial:
2392
+ type:
2393
+ - boolean
2394
+ - 'null'
2395
+ pointOfContactName:
2396
+ type:
2397
+ - string
2398
+ - 'null'
2399
+ pointOfContactTitle:
2400
+ type:
2401
+ - string
2402
+ - 'null'
2403
+ pointOfContactPhone:
2404
+ type:
2405
+ - string
2406
+ - 'null'
2407
+ primaryEmail:
2408
+ type:
2409
+ - string
2410
+ - 'null'
2411
+ propertyAddress:
2412
+ type:
2413
+ - string
2414
+ - 'null'
2415
+ leadSource:
2416
+ type:
2417
+ - string
2418
+ - 'null'
2419
+ leadSourceReferralId:
2420
+ type:
2421
+ - integer
2422
+ - 'null'
2423
+ minimum: -2147483648
2424
+ maximum: 2147483647
2425
+ leadSourceReferralUserId:
2426
+ type:
2427
+ - string
2428
+ - 'null'
2429
+ leadSourceOther:
2430
+ type:
2431
+ - string
2432
+ - 'null'
2433
+ googleDriveCustomerFolderId:
2434
+ type:
2435
+ - string
2436
+ - 'null'
2437
+ cachedLat:
2438
+ type:
2439
+ - number
2440
+ - 'null'
2441
+ minimum: -140737488355328
2442
+ maximum: 140737488355327
2443
+ cachedLng:
2444
+ type:
2445
+ - number
2446
+ - 'null'
2447
+ minimum: -140737488355328
2448
+ maximum: 140737488355327
2449
+ qboId:
2450
+ type:
2451
+ - string
2452
+ - 'null'
2453
+ qboSyncToken:
2454
+ type:
2455
+ - string
2456
+ - 'null'
2457
+ qboLastPushedAt:
2458
+ type:
2459
+ - string
2460
+ - 'null'
2461
+ qboLastPushedHash:
2462
+ type:
2463
+ - string
2464
+ - 'null'
2465
+ qboLastError:
2466
+ type:
2467
+ - string
2468
+ - 'null'
2469
+ deletedAt:
2470
+ type:
2471
+ - string
2472
+ - 'null'
2473
+ deletedBy:
2474
+ type:
2475
+ - string
2476
+ - 'null'
2477
+ required:
2478
+ - name
2479
+ responses:
2480
+ '201':
2481
+ description: Created.
2482
+ content:
2483
+ application/json:
2484
+ schema:
2485
+ allOf:
2486
+ - type: object
2487
+ properties:
2488
+ userId:
2489
+ type:
2490
+ - string
2491
+ - 'null'
2492
+ organizationId:
2493
+ type:
2494
+ - string
2495
+ - 'null'
2496
+ customerNumber:
2497
+ type:
2498
+ - integer
2499
+ - 'null'
2500
+ minimum: -2147483648
2501
+ maximum: 2147483647
2502
+ name:
2503
+ type: string
2504
+ email:
2505
+ type:
2506
+ - string
2507
+ - 'null'
2508
+ phone:
2509
+ type:
2510
+ - string
2511
+ - 'null'
2512
+ company:
2513
+ type:
2514
+ - string
2515
+ - 'null'
2516
+ address:
2517
+ type:
2518
+ - string
2519
+ - 'null'
2520
+ status:
2521
+ type:
2522
+ - string
2523
+ - 'null'
2524
+ customFields:
2525
+ anyOf:
2526
+ - type: string
2527
+ - type: number
2528
+ - type: boolean
2529
+ - type: 'null'
2530
+ - type: object
2531
+ additionalProperties: {}
2532
+ - type: array
2533
+ items: {}
2534
+ - type: 'null'
2535
+ lastContactDate:
2536
+ type:
2537
+ - string
2538
+ - 'null'
2539
+ parentContactId:
2540
+ type:
2541
+ - integer
2542
+ - 'null'
2543
+ minimum: -2147483648
2544
+ maximum: 2147483647
2545
+ relationship:
2546
+ type:
2547
+ - string
2548
+ - 'null'
2549
+ role:
2550
+ type:
2551
+ - string
2552
+ - 'null'
2553
+ firstName:
2554
+ type:
2555
+ - string
2556
+ - 'null'
2557
+ lastName:
2558
+ type:
2559
+ - string
2560
+ - 'null'
2561
+ serviceAddress:
2562
+ type:
2563
+ - string
2564
+ - 'null'
2565
+ billingAddress:
2566
+ type:
2567
+ - string
2568
+ - 'null'
2569
+ isCommercial:
2570
+ type:
2571
+ - boolean
2572
+ - 'null'
2573
+ pointOfContactName:
2574
+ type:
2575
+ - string
2576
+ - 'null'
2577
+ pointOfContactTitle:
2578
+ type:
2579
+ - string
2580
+ - 'null'
2581
+ pointOfContactPhone:
2582
+ type:
2583
+ - string
2584
+ - 'null'
2585
+ primaryEmail:
2586
+ type:
2587
+ - string
2588
+ - 'null'
2589
+ propertyAddress:
2590
+ type:
2591
+ - string
2592
+ - 'null'
2593
+ leadSource:
2594
+ type:
2595
+ - string
2596
+ - 'null'
2597
+ leadSourceReferralId:
2598
+ type:
2599
+ - integer
2600
+ - 'null'
2601
+ minimum: -2147483648
2602
+ maximum: 2147483647
2603
+ leadSourceReferralUserId:
2604
+ type:
2605
+ - string
2606
+ - 'null'
2607
+ leadSourceOther:
2608
+ type:
2609
+ - string
2610
+ - 'null'
2611
+ googleDriveCustomerFolderId:
2612
+ type:
2613
+ - string
2614
+ - 'null'
2615
+ cachedLat:
2616
+ type:
2617
+ - number
2618
+ - 'null'
2619
+ minimum: -140737488355328
2620
+ maximum: 140737488355327
2621
+ cachedLng:
2622
+ type:
2623
+ - number
2624
+ - 'null'
2625
+ minimum: -140737488355328
2626
+ maximum: 140737488355327
2627
+ qboId:
2628
+ type:
2629
+ - string
2630
+ - 'null'
2631
+ qboSyncToken:
2632
+ type:
2633
+ - string
2634
+ - 'null'
2635
+ qboLastPushedAt:
2636
+ type:
2637
+ - string
2638
+ - 'null'
2639
+ qboLastPushedHash:
2640
+ type:
2641
+ - string
2642
+ - 'null'
2643
+ qboLastError:
2644
+ type:
2645
+ - string
2646
+ - 'null'
2647
+ deletedAt:
2648
+ type:
2649
+ - string
2650
+ - 'null'
2651
+ deletedBy:
2652
+ type:
2653
+ - string
2654
+ - 'null'
2655
+ required:
2656
+ - name
2657
+ - type: object
2658
+ properties:
2659
+ id:
2660
+ type: number
2661
+ required:
2662
+ - id
2663
+ '400':
2664
+ description: Validation failure.
2665
+ content:
2666
+ application/json:
2667
+ schema:
2668
+ $ref: '#/components/schemas/ErrorEnvelope'
2669
+ /api/jobs/{jobId}/photos:
2670
+ post:
2671
+ summary: Upload a project photo (raw-first; server watermarks).
2672
+ tags:
2673
+ - photos
2674
+ parameters:
2675
+ - *ref_1
2676
+ - schema:
2677
+ type: integer
2678
+ exclusiveMinimum: 0
2679
+ required: true
2680
+ name: jobId
2681
+ in: path
2682
+ requestBody:
2683
+ content:
2684
+ multipart/form-data:
2685
+ schema:
2686
+ type: object
2687
+ properties:
2688
+ file:
2689
+ type: string
2690
+ format: binary
2691
+ required:
2692
+ - file
2693
+ responses:
2694
+ '201':
2695
+ description: Photo created.
2696
+ content:
2697
+ application/json:
2698
+ schema:
2699
+ $ref: '#/components/schemas/PhotoUploadResponse'
2700
+ '400':
2701
+ description: Validation failure.
2702
+ content:
2703
+ application/json:
2704
+ schema:
2705
+ $ref: '#/components/schemas/ErrorEnvelope'
2706
+ '403':
2707
+ description: Caller not assigned to this job.
2708
+ content:
2709
+ application/json:
2710
+ schema:
2711
+ $ref: '#/components/schemas/ErrorEnvelope'
2712
+ /api/jobs/{jobId}/scans:
2713
+ get:
2714
+ summary: List every scan on a job. Augments each row with floor placement echo.
2715
+ tags:
2716
+ - lidar
2717
+ parameters:
2718
+ - *ref_1
2719
+ - schema:
2720
+ type: integer
2721
+ exclusiveMinimum: 0
2722
+ required: true
2723
+ name: jobId
2724
+ in: path
2725
+ responses:
2726
+ '200':
2727
+ description: Sorted by COALESCE(captureTimestamp, createdAt) DESC.
2728
+ content:
2729
+ application/json:
2730
+ schema:
2731
+ type: array
2732
+ items:
2733
+ type: object
2734
+ additionalProperties: {}
2735
+ webhooks: {}