@serviceai/api-spec 1.2.418 → 1.2.420

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.
Files changed (4) hide show
  1. package/openapi.json +791 -41
  2. package/openapi.yaml +720 -39
  3. package/package.json +1 -1
  4. package/types.d.ts +382 -33
package/types.d.ts CHANGED
@@ -1065,7 +1065,7 @@ export interface paths {
1065
1065
  "application/json": ({
1066
1066
  [key: string]: unknown;
1067
1067
  } & {
1068
- /** @description RoomPlan-classified doors. Distinct from `windows[]` and `openings[]`; append-room `entryDoorwayId` tokens of the form `…-door-<idx>` resolve into THIS array only. */
1068
+ /** @description RoomPlan-classified doors. Distinct from `windows[]` and `openings[]`; append-room `entryDoorwayId` tokens of the form `…-door-<idx>` resolve into THIS array only. A door carrying `duplicateOf` is the hidden copy of a shared doorway (one door per doorway, from the room scanned first). */
1069
1069
  doors?: {
1070
1070
  position: {
1071
1071
  x: number;
@@ -1079,6 +1079,13 @@ export interface paths {
1079
1079
  swingHand?: "left" | "right" | "none";
1080
1080
  /** @enum {string} */
1081
1081
  placementSource?: "auto" | "manual";
1082
+ /** @description Present only on the later-scanned room's copy of a doorway two rooms in this response share (the rooms are linked by a door link, or their doors coincide on one wall). Skip this door's glyph and keep its wall gap; the door it names is the one to draw. Absent on every other door. Read-time only: never send it back, it is never stored. */
1083
+ duplicateOf?: {
1084
+ /** @description Id of the room whose copy of this doorway is drawn. */
1085
+ roomId: number;
1086
+ /** @description Index into that room's `doors[]`. */
1087
+ doorIndex: number;
1088
+ };
1082
1089
  }[];
1083
1090
  /** @description RoomPlan-classified windows. Distinct from `doors[]` and `openings[]`; no append-room `entryDoorwayId` ever resolves into this array (windows are not traversable). */
1084
1091
  windows?: {
@@ -1196,7 +1203,7 @@ export interface paths {
1196
1203
  rooms: ({
1197
1204
  [key: string]: unknown;
1198
1205
  } & {
1199
- /** @description RoomPlan-classified doors. Distinct from `windows[]` and `openings[]`; append-room `entryDoorwayId` tokens of the form `…-door-<idx>` resolve into THIS array only. */
1206
+ /** @description RoomPlan-classified doors. Distinct from `windows[]` and `openings[]`; append-room `entryDoorwayId` tokens of the form `…-door-<idx>` resolve into THIS array only. A door carrying `duplicateOf` is the hidden copy of a shared doorway (one door per doorway, from the room scanned first). */
1200
1207
  doors?: {
1201
1208
  position: {
1202
1209
  x: number;
@@ -1210,6 +1217,13 @@ export interface paths {
1210
1217
  swingHand?: "left" | "right" | "none";
1211
1218
  /** @enum {string} */
1212
1219
  placementSource?: "auto" | "manual";
1220
+ /** @description Present only on the later-scanned room's copy of a doorway two rooms in this response share (the rooms are linked by a door link, or their doors coincide on one wall). Skip this door's glyph and keep its wall gap; the door it names is the one to draw. Absent on every other door. Read-time only: never send it back, it is never stored. */
1221
+ duplicateOf?: {
1222
+ /** @description Id of the room whose copy of this doorway is drawn. */
1223
+ roomId: number;
1224
+ /** @description Index into that room's `doors[]`. */
1225
+ doorIndex: number;
1226
+ };
1213
1227
  }[];
1214
1228
  /** @description RoomPlan-classified windows. Distinct from `doors[]` and `openings[]`; no append-room `entryDoorwayId` ever resolves into this array (windows are not traversable). */
1215
1229
  windows?: {
@@ -1929,9 +1943,15 @@ export interface paths {
1929
1943
  device?: "ios" | "web" | "android" | null;
1930
1944
  /**
1931
1945
  * Format: date-time
1932
- * @description When the tap happened. The server clamps it.
1946
+ * @description When the tap happened, by the phone's clock. The server checks it against its own (tapElapsedMs) and clamps it. From the web (device 'web') it is ignored: the server's receipt time is used.
1933
1947
  */
1934
1948
  occurredAt?: string | null;
1949
+ /** @description Milliseconds from the tap to this send, from the phone's monotonic clock (a stopwatch the person can't change). An offline replay sends the time since the tap. The server's tap time is its own clock when the request arrives minus this. Within 2 minutes of the phone's time, the phone's time stands (never later than the tap); further apart, the server's tap time is recorded and the office is asked to look. A Clock Out or job time stop it proves this way keeps its real time however old; without it, a stop older than 12 hours is recorded at the 12-hour limit and flagged (T6). Send it with every queued tap, clock-outs included. */
1950
+ tapElapsedMs?: number | null;
1951
+ /** @description True when the phone restarted between the tap and this send, so there is no stopwatch reading (send no tapElapsedMs). A tap more than 2 minutes old keeps the phone's time and is flagged for the office. */
1952
+ tapTimeUnverified?: boolean | null;
1953
+ /** @description True when the phone's clock was changed by hand since the phone last synced. The tap is flagged for the office. */
1954
+ deviceClockChanged?: boolean | null;
1935
1955
  notes?: string | null;
1936
1956
  };
1937
1957
  };
@@ -1964,7 +1984,7 @@ export interface paths {
1964
1984
  "application/json": components["schemas"]["ErrorEnvelope"];
1965
1985
  };
1966
1986
  };
1967
- /** @description Time tracking access denied, or no organization on the session. */
1987
+ /** @description Time tracking access denied, no organization on the session, or code PAYROLL_OFF: the company has the time tracking add-on off (GET /api/time-tracking/day says payrollAddon: false). Don't retry; hide Clock In/Out and the other payroll controls. */
1968
1988
  403: {
1969
1989
  headers: {
1970
1990
  [name: string]: unknown;
@@ -2013,7 +2033,10 @@ export interface paths {
2013
2033
  };
2014
2034
  get?: never;
2015
2035
  put?: never;
2016
- /** End my day. Closes every open entry with it. */
2036
+ /**
2037
+ * End my day: the paid day only. Job time (fence or job timer) runs until it is stopped or the phone reports leaving, and is never paid past Clock Out.
2038
+ * @description A late Clock Out (T6): with tapElapsedMs proving its time, it keeps its real time however old. When the safety net already closed the day (the automatic close), a proven Clock Out earlier than that close replaces it (replacedAutoClose, flagged for the office); anything else about that day is flagged for the office and changes nothing. A proven Clock Out from before the running day started never ends the running day.
2039
+ */
2017
2040
  post: {
2018
2041
  parameters: {
2019
2042
  query?: never;
@@ -2036,15 +2059,21 @@ export interface paths {
2036
2059
  device?: "ios" | "web" | "android" | null;
2037
2060
  /**
2038
2061
  * Format: date-time
2039
- * @description When the tap happened. The server clamps it.
2062
+ * @description When the tap happened, by the phone's clock. The server checks it against its own (tapElapsedMs) and clamps it. From the web (device 'web') it is ignored: the server's receipt time is used.
2040
2063
  */
2041
2064
  occurredAt?: string | null;
2065
+ /** @description Milliseconds from the tap to this send, from the phone's monotonic clock (a stopwatch the person can't change). An offline replay sends the time since the tap. The server's tap time is its own clock when the request arrives minus this. Within 2 minutes of the phone's time, the phone's time stands (never later than the tap); further apart, the server's tap time is recorded and the office is asked to look. A Clock Out or job time stop it proves this way keeps its real time however old; without it, a stop older than 12 hours is recorded at the 12-hour limit and flagged (T6). Send it with every queued tap, clock-outs included. */
2066
+ tapElapsedMs?: number | null;
2067
+ /** @description True when the phone restarted between the tap and this send, so there is no stopwatch reading (send no tapElapsedMs). A tap more than 2 minutes old keeps the phone's time and is flagged for the office. */
2068
+ tapTimeUnverified?: boolean | null;
2069
+ /** @description True when the phone's clock was changed by hand since the phone last synced. The tap is flagged for the office. */
2070
+ deviceClockChanged?: boolean | null;
2042
2071
  note?: string | null;
2043
2072
  };
2044
2073
  };
2045
2074
  };
2046
2075
  responses: {
2047
- /** @description Day ended. */
2076
+ /** @description Day ended, or the automatic close of the day this tap belongs to replaced (replacedAutoClose). */
2048
2077
  200: {
2049
2078
  headers: {
2050
2079
  [name: string]: unknown;
@@ -2052,7 +2081,7 @@ export interface paths {
2052
2081
  content: {
2053
2082
  "application/json": {
2054
2083
  day: components["schemas"]["WorkDay"] & (Record<string, never> | null);
2055
- closedEntries: number;
2084
+ replacedAutoClose?: boolean;
2056
2085
  };
2057
2086
  };
2058
2087
  };
@@ -2065,7 +2094,7 @@ export interface paths {
2065
2094
  "application/json": components["schemas"]["ErrorEnvelope"];
2066
2095
  };
2067
2096
  };
2068
- /** @description Time tracking access denied, or no organization on the session. */
2097
+ /** @description Time tracking access denied, no organization on the session, or code PAYROLL_OFF: the company has the time tracking add-on off (GET /api/time-tracking/day says payrollAddon: false). Don't retry; hide Clock In/Out and the other payroll controls. */
2069
2098
  403: {
2070
2099
  headers: {
2071
2100
  [name: string]: unknown;
@@ -2074,7 +2103,7 @@ export interface paths {
2074
2103
  "application/json": components["schemas"]["ErrorEnvelope"];
2075
2104
  };
2076
2105
  };
2077
- /** @description No day is running (code NO_OPEN_DAY). */
2106
+ /** @description No day is running (code NO_OPEN_DAY). Also when the tap was only flagged for the office. */
2078
2107
  404: {
2079
2108
  headers: {
2080
2109
  [name: string]: unknown;
@@ -2083,7 +2112,7 @@ export interface paths {
2083
2112
  "application/json": components["schemas"]["ErrorEnvelope"];
2084
2113
  };
2085
2114
  };
2086
- /** @description The day was already closed. */
2115
+ /** @description The day was already closed; or code DAY_CLOSED_AUTOMATICALLY: the tap belongs to an earlier day the safety net closed, and was flagged for the office. */
2087
2116
  409: {
2088
2117
  headers: {
2089
2118
  [name: string]: unknown;
@@ -2306,7 +2335,7 @@ export interface paths {
2306
2335
  };
2307
2336
  requestBody?: never;
2308
2337
  responses: {
2309
- /** @description Fences, office first when requested. */
2338
+ /** @description Fences, office first when requested. No job fences for someone the office turned job-site tracking off for (clock-config jobSiteTracking false), except with includeClosed for the manual picker. */
2310
2339
  200: {
2311
2340
  headers: {
2312
2341
  [name: string]: unknown;
@@ -2359,12 +2388,12 @@ export interface paths {
2359
2388
  path?: never;
2360
2389
  cookie?: never;
2361
2390
  };
2362
- /** My open entries right now (or the team's). */
2391
+ /** My open entries right now, or who is on the clock. */
2363
2392
  get: {
2364
2393
  parameters: {
2365
2394
  query?: {
2366
- /** @description Everyone on the clock. Requires time_tracking.view_all. */
2367
- scope?: "team";
2395
+ /** @description `team`: everyone on the clock, one OnTheClockRow per person; requires time_tracking.view_all. `me`: the caller's own row while on the clock. Without scope: the caller's open entries. */
2396
+ scope?: "team" | "me";
2368
2397
  };
2369
2398
  header?: {
2370
2399
  /** @description Semver of the calling app build. Drives the 426 / soft-upgrade handshake. */
@@ -2377,13 +2406,13 @@ export interface paths {
2377
2406
  };
2378
2407
  requestBody?: never;
2379
2408
  responses: {
2380
- /** @description Open entries. Normally zero or one for a single user. */
2409
+ /** @description Without scope: open entries, normally zero or one. With scope: one OnTheClockRow per person on the clock. */
2381
2410
  200: {
2382
2411
  headers: {
2383
2412
  [name: string]: unknown;
2384
2413
  };
2385
2414
  content: {
2386
- "application/json": components["schemas"]["TimeEntry"][];
2415
+ "application/json": components["schemas"]["TimeEntry"][] | components["schemas"]["OnTheClockRow"][];
2387
2416
  };
2388
2417
  };
2389
2418
  /** @description Not signed in. */
@@ -2509,8 +2538,8 @@ export interface paths {
2509
2538
  get?: never;
2510
2539
  put?: never;
2511
2540
  /**
2512
- * Clock in to a job or the office by hand.
2513
- * @description A deliberate button press. On the day model a press with no day running starts the day at that moment; a fence-driven clock-in from an old-model client cannot, and is refused.
2541
+ * Start time on a job or at the office by hand.
2542
+ * @description A deliberate button press. The entry joins the person's work day when one is running. With none running: a job timer press (startDay: false) records job time only and never starts the paid day; a Clock In press (startDay omitted: older iPhones' job picker, the office's "Clock X in") starts the day at that moment; an automatic clock-in is refused with NO_OPEN_DAY, because the fence crossing already records the arrival. A person starting their own job time away from the job's site, or with no usable location, is asked first (send canConfirmAway: 409 JOB_TIME_AWAY carries the question; resend with confirmAway once they say yes) and the entry is flagged (startCheck) for the office to approve, move or not count. Older apps start straight away, flagged the same.
2514
2543
  */
2515
2544
  post: {
2516
2545
  parameters: {
@@ -2535,20 +2564,34 @@ export interface paths {
2535
2564
  siteType?: "job" | "office";
2536
2565
  /** @description Clock someone else in. Requires time_tracking.edit_all. */
2537
2566
  userId?: string;
2567
+ /** @description Why, when clocking someone else in. Required with userId. */
2568
+ note?: string;
2538
2569
  lat?: number;
2539
2570
  lng?: number;
2540
2571
  /** @enum {string} */
2541
2572
  source?: "manual" | "auto";
2573
+ /** @description false: a job timer press, job time only; it never starts the paid day. Omitted: a Clock In press starts the day when none is running. */
2574
+ startDay?: boolean;
2542
2575
  notes?: string;
2543
2576
  budgetItemId?: number | null;
2544
2577
  /**
2545
2578
  * Format: date-time
2546
- * @description When the tech actually arrived. The server clamps it.
2579
+ * @description When the tech actually arrived, by the phone's clock. The server checks it against its own (tapElapsedMs) and clamps it. From the web (device 'web') it is ignored: the server's receipt time is used.
2547
2580
  */
2548
2581
  occurredAt?: string;
2549
2582
  accuracyM?: number | null;
2550
2583
  /** @enum {string|null} */
2551
2584
  device?: "ios" | "web" | "android" | null;
2585
+ /** @description This app can ask the person before starting job time away from the job (it handles 409 JOB_TIME_AWAY). */
2586
+ canConfirmAway?: boolean;
2587
+ /** @description The person said yes to starting away from the job. */
2588
+ confirmAway?: boolean;
2589
+ /** @description Milliseconds from the tap to this send, from the phone's monotonic clock (a stopwatch the person can't change). An offline replay sends the time since the tap. The server's tap time is its own clock when the request arrives minus this. Within 2 minutes of the phone's time, the phone's time stands (never later than the tap); further apart, the server's tap time is recorded and the office is asked to look. A Clock Out or job time stop it proves this way keeps its real time however old; without it, a stop older than 12 hours is recorded at the 12-hour limit and flagged (T6). Send it with every queued tap, clock-outs included. */
2590
+ tapElapsedMs?: number | null;
2591
+ /** @description True when the phone restarted between the tap and this send, so there is no stopwatch reading (send no tapElapsedMs). A tap more than 2 minutes old keeps the phone's time and is flagged for the office. */
2592
+ tapTimeUnverified?: boolean | null;
2593
+ /** @description True when the phone's clock was changed by hand since the phone last synced. The tap is flagged for the office. */
2594
+ deviceClockChanged?: boolean | null;
2552
2595
  };
2553
2596
  };
2554
2597
  };
@@ -2562,7 +2605,7 @@ export interface paths {
2562
2605
  "application/json": components["schemas"]["TimeEntry"];
2563
2606
  };
2564
2607
  };
2565
- /** @description Validation failure, or jobId missing on a job clock-in. */
2608
+ /** @description Validation failure, jobId missing on a job clock-in, or no note when clocking someone else in. */
2566
2609
  400: {
2567
2610
  headers: {
2568
2611
  [name: string]: unknown;
@@ -2580,7 +2623,7 @@ export interface paths {
2580
2623
  "application/json": components["schemas"]["ErrorEnvelope"];
2581
2624
  };
2582
2625
  };
2583
- /** @description Time tracking access denied, or no organization on the session. */
2626
+ /** @description Time tracking access denied, or no organization on the session. PAYROLL_OFF: a Clock In press that would start the day at a company with the time tracking add-on off. JOB_TIME_OFF: the company has both the time tracking add-on and location tracking off, so job time isn't used; stopping running job time still works. */
2584
2627
  403: {
2585
2628
  headers: {
2586
2629
  [name: string]: unknown;
@@ -2598,7 +2641,7 @@ export interface paths {
2598
2641
  "application/json": components["schemas"]["ErrorEnvelope"];
2599
2642
  };
2600
2643
  };
2601
- /** @description Already clocked in at this site (code ALREADY_CLOCKED_IN_SAME_JOB). The body carries the existing entry — adopt it. */
2644
+ /** @description ALREADY_CLOCKED_IN_SAME_JOB: already at this site; the body carries the existing entry — adopt it. ALREADY_CLOCKED_IN_OTHER_JOB: an automatic clock-in while another entry is open; the body carries it. NO_OPEN_DAY: an automatic clock-in with no work day running; the fence crossing records the arrival. JOB_SITE_TRACKING_OFF: an automatic clock-in for someone the office turned job-site tracking off for; start job time by hand. PERIOD_LOCKED: an admin clock-in inside a locked pay period. JOB_TIME_AWAY: starting job time away from the job (reason 'away', with distanceM) or with no usable location (reason 'no_location'); show message as the question and resend with confirmAway: true. */
2602
2645
  409: {
2603
2646
  headers: {
2604
2647
  [name: string]: unknown;
@@ -2607,7 +2650,11 @@ export interface paths {
2607
2650
  "application/json": {
2608
2651
  message: string;
2609
2652
  code: string;
2610
- entry: components["schemas"]["TimeEntry"];
2653
+ entry?: components["schemas"]["TimeEntry"];
2654
+ /** @enum {string} */
2655
+ reason?: "away" | "no_location";
2656
+ distanceM?: number | null;
2657
+ jobName?: string | null;
2611
2658
  };
2612
2659
  };
2613
2660
  };
@@ -2667,17 +2714,23 @@ export interface paths {
2667
2714
  notes?: string;
2668
2715
  /**
2669
2716
  * Format: date-time
2670
- * @description When the tech actually left. Clamped to [entry.clockIn, now].
2717
+ * @description When the tech actually left, by the phone's clock. Checked against the server's (tapElapsedMs) and clamped to [entry.clockIn, now], and to the last 12 hours unless tapElapsedMs proves it (T6). From the web (device 'web') it is ignored: the server's receipt time is used.
2671
2718
  */
2672
2719
  occurredAt?: string;
2673
2720
  accuracyM?: number | null;
2674
2721
  /** @enum {string|null} */
2675
2722
  device?: "ios" | "web" | "android" | null;
2723
+ /** @description Milliseconds from the tap to this send, from the phone's monotonic clock (a stopwatch the person can't change). An offline replay sends the time since the tap. The server's tap time is its own clock when the request arrives minus this. Within 2 minutes of the phone's time, the phone's time stands (never later than the tap); further apart, the server's tap time is recorded and the office is asked to look. A Clock Out or job time stop it proves this way keeps its real time however old; without it, a stop older than 12 hours is recorded at the 12-hour limit and flagged (T6). Send it with every queued tap, clock-outs included. */
2724
+ tapElapsedMs?: number | null;
2725
+ /** @description True when the phone restarted between the tap and this send, so there is no stopwatch reading (send no tapElapsedMs). A tap more than 2 minutes old keeps the phone's time and is flagged for the office. */
2726
+ tapTimeUnverified?: boolean | null;
2727
+ /** @description True when the phone's clock was changed by hand since the phone last synced. The tap is flagged for the office. */
2728
+ deviceClockChanged?: boolean | null;
2676
2729
  };
2677
2730
  };
2678
2731
  };
2679
2732
  responses: {
2680
- /** @description Entry closed. */
2733
+ /** @description Entry closed; or, for a stop tapElapsedMs proves that is earlier than the automatic close of the job time there, that close replaced (T6). */
2681
2734
  200: {
2682
2735
  headers: {
2683
2736
  [name: string]: unknown;
@@ -2713,7 +2766,7 @@ export interface paths {
2713
2766
  "application/json": components["schemas"]["ErrorEnvelope"];
2714
2767
  };
2715
2768
  };
2716
- /** @description No open entry at that site. */
2769
+ /** @description No open entry at that site. Also when the stop belongs to job time the safety net closed and was only flagged for the office. */
2717
2770
  404: {
2718
2771
  headers: {
2719
2772
  [name: string]: unknown;
@@ -2750,7 +2803,7 @@ export interface paths {
2750
2803
  put?: never;
2751
2804
  /**
2752
2805
  * Move my clock to another job in one atomic call.
2753
- * @description Closes the caller's open entry and opens one on the target. One request instead of clock-out + clock-in, so an offline drop between the two cannot strand the tech clocked out. Own entry only.
2806
+ * @description Closes the caller's open entry and opens one on the target. One request instead of clock-out + clock-in, so an offline drop between the two cannot strand the tech clocked out. Own entry only. Moving job time to a job the person isn't at, or with no usable location, is asked first (send canConfirmAway: 409 JOB_TIME_AWAY carries the question; resend with confirmAway once they say yes) and the new entry is flagged (startCheck) for the office. Older apps move straight away, flagged the same. switchAt is a time the person picked, so earlier than the tap is normal. With tapElapsedMs it is never later than the tap by the server's clock, and more than 2 minutes later is replaced by the tap time and flagged (opened.clockInCheck). tapTimeUnverified doesn't apply; deviceClockChanged flags it.
2754
2807
  */
2755
2808
  post: {
2756
2809
  parameters: {
@@ -2773,6 +2826,17 @@ export interface paths {
2773
2826
  switchAt?: string;
2774
2827
  lat?: number;
2775
2828
  lng?: number;
2829
+ accuracyM?: number | null;
2830
+ /** @description This app can ask the person before moving job time to a job they aren't at (it handles 409 JOB_TIME_AWAY). */
2831
+ canConfirmAway?: boolean;
2832
+ /** @description The person said yes to moving it anyway. */
2833
+ confirmAway?: boolean;
2834
+ /** @description Milliseconds from the tap to this send, from the phone's monotonic clock (a stopwatch the person can't change). An offline replay sends the time since the tap. The server's tap time is its own clock when the request arrives minus this. Within 2 minutes of the phone's time, the phone's time stands (never later than the tap); further apart, the server's tap time is recorded and the office is asked to look. A Clock Out or job time stop it proves this way keeps its real time however old; without it, a stop older than 12 hours is recorded at the 12-hour limit and flagged (T6). Send it with every queued tap, clock-outs included. */
2835
+ tapElapsedMs?: number | null;
2836
+ /** @description True when the phone restarted between the tap and this send, so there is no stopwatch reading (send no tapElapsedMs). A tap more than 2 minutes old keeps the phone's time and is flagged for the office. */
2837
+ tapTimeUnverified?: boolean | null;
2838
+ /** @description True when the phone's clock was changed by hand since the phone last synced. The tap is flagged for the office. */
2839
+ deviceClockChanged?: boolean | null;
2776
2840
  };
2777
2841
  };
2778
2842
  };
@@ -2808,7 +2872,7 @@ export interface paths {
2808
2872
  "application/json": components["schemas"]["ErrorEnvelope"];
2809
2873
  };
2810
2874
  };
2811
- /** @description Time tracking access denied, or no organization on the session. */
2875
+ /** @description Time tracking access denied, or no organization on the session. JOB_TIME_OFF: the company has both the time tracking add-on and location tracking off, so job time isn't used; stopping running job time still works. */
2812
2876
  403: {
2813
2877
  headers: {
2814
2878
  [name: string]: unknown;
@@ -2826,6 +2890,22 @@ export interface paths {
2826
2890
  "application/json": components["schemas"]["ErrorEnvelope"];
2827
2891
  };
2828
2892
  };
2893
+ /** @description PERIOD_LOCKED: the switch time is inside a locked pay period. JOB_TIME_AWAY: the person isn't at the target job (reason 'away', with distanceM) or has no usable location (reason 'no_location'); nothing moved. Show message as the question and resend with confirmAway: true. */
2894
+ 409: {
2895
+ headers: {
2896
+ [name: string]: unknown;
2897
+ };
2898
+ content: {
2899
+ "application/json": {
2900
+ message: string;
2901
+ code: string;
2902
+ /** @enum {string} */
2903
+ reason?: "away" | "no_location";
2904
+ distanceM?: number | null;
2905
+ jobName?: string | null;
2906
+ };
2907
+ };
2908
+ };
2829
2909
  /** @description Client major below minClientVersion. */
2830
2910
  426: {
2831
2911
  headers: {
@@ -3127,7 +3207,7 @@ export interface paths {
3127
3207
  "application/json": components["schemas"]["ErrorEnvelope"];
3128
3208
  };
3129
3209
  };
3130
- /** @description Time tracking access denied, or no organization on the session. */
3210
+ /** @description Time tracking access denied, no organization on the session, or code PAYROLL_OFF: the company has the time tracking add-on off (GET /api/time-tracking/day says payrollAddon: false). Don't retry; hide Clock In/Out and the other payroll controls. */
3131
3211
  403: {
3132
3212
  headers: {
3133
3213
  [name: string]: unknown;
@@ -3369,6 +3449,15 @@ export interface paths {
3369
3449
  "application/json": components["schemas"]["ErrorEnvelope"];
3370
3450
  };
3371
3451
  };
3452
+ /** @description Time tracking access denied, no organization on the session, or code PAYROLL_OFF: the company has the time tracking add-on off (GET /api/time-tracking/day says payrollAddon: false). Don't retry; hide Clock In/Out and the other payroll controls. */
3453
+ 403: {
3454
+ headers: {
3455
+ [name: string]: unknown;
3456
+ };
3457
+ content: {
3458
+ "application/json": components["schemas"]["ErrorEnvelope"];
3459
+ };
3460
+ };
3372
3461
  /** @description A request for those dates is already waiting. */
3373
3462
  409: {
3374
3463
  headers: {
@@ -3395,6 +3484,171 @@ export interface paths {
3395
3484
  patch?: never;
3396
3485
  trace?: never;
3397
3486
  };
3487
+ "/api/account/deletion-request": {
3488
+ parameters: {
3489
+ query?: never;
3490
+ header?: never;
3491
+ path?: never;
3492
+ cookie?: never;
3493
+ };
3494
+ /** My newest account deletion request. */
3495
+ get: {
3496
+ parameters: {
3497
+ query?: never;
3498
+ header?: {
3499
+ /** @description Semver of the calling app build. Drives the 426 / soft-upgrade handshake. */
3500
+ "X-Client-Version"?: string;
3501
+ };
3502
+ path?: never;
3503
+ cookie?: never;
3504
+ };
3505
+ requestBody?: never;
3506
+ responses: {
3507
+ /** @description The newest request, whatever its status. */
3508
+ 200: {
3509
+ headers: {
3510
+ [name: string]: unknown;
3511
+ };
3512
+ content: {
3513
+ "application/json": components["schemas"]["AccountDeletionRequest"];
3514
+ };
3515
+ };
3516
+ /** @description Not signed in. */
3517
+ 401: {
3518
+ headers: {
3519
+ [name: string]: unknown;
3520
+ };
3521
+ content: {
3522
+ "application/json": components["schemas"]["ErrorEnvelope"];
3523
+ };
3524
+ };
3525
+ /** @description No organization on the session. */
3526
+ 403: {
3527
+ headers: {
3528
+ [name: string]: unknown;
3529
+ };
3530
+ content: {
3531
+ "application/json": components["schemas"]["ErrorEnvelope"];
3532
+ };
3533
+ };
3534
+ /** @description No request yet. */
3535
+ 404: {
3536
+ headers: {
3537
+ [name: string]: unknown;
3538
+ };
3539
+ content: {
3540
+ "application/json": {
3541
+ /** @example No deletion request */
3542
+ message: string;
3543
+ };
3544
+ };
3545
+ };
3546
+ /** @description Client major below minClientVersion. */
3547
+ 426: {
3548
+ headers: {
3549
+ [name: string]: unknown;
3550
+ };
3551
+ content: {
3552
+ "application/json": components["schemas"]["UpgradeRequiredEnvelope"];
3553
+ };
3554
+ };
3555
+ };
3556
+ };
3557
+ put?: never;
3558
+ /**
3559
+ * Ask for my account to be deleted.
3560
+ * @description Nothing is deleted by this call. The request goes to the company's owner and Team admins (bell, push and email) and to ServiceAi support (email); the company removes the account. Asking again while a request is pending answers that request with 200 and creates nothing; everyone is reminded at most once a day. The reason is never put in a push.
3561
+ */
3562
+ post: {
3563
+ parameters: {
3564
+ query?: never;
3565
+ header?: {
3566
+ /** @description Semver of the calling app build. Drives the 426 / soft-upgrade handshake. */
3567
+ "X-Client-Version"?: string;
3568
+ };
3569
+ path?: never;
3570
+ cookie?: never;
3571
+ };
3572
+ requestBody?: {
3573
+ content: {
3574
+ "application/json": {
3575
+ /** @description Optional. Trimmed; at most 1000 characters after trimming. */
3576
+ reason?: string | null;
3577
+ };
3578
+ };
3579
+ };
3580
+ responses: {
3581
+ /** @description A request was already pending: this is that request. */
3582
+ 200: {
3583
+ headers: {
3584
+ [name: string]: unknown;
3585
+ };
3586
+ content: {
3587
+ "application/json": components["schemas"]["AccountDeletionRequest"];
3588
+ };
3589
+ };
3590
+ /** @description Request created. */
3591
+ 202: {
3592
+ headers: {
3593
+ [name: string]: unknown;
3594
+ };
3595
+ content: {
3596
+ "application/json": components["schemas"]["AccountDeletionRequest"];
3597
+ };
3598
+ };
3599
+ /** @description Validation failure (a reason longer than 1000 characters). */
3600
+ 400: {
3601
+ headers: {
3602
+ [name: string]: unknown;
3603
+ };
3604
+ content: {
3605
+ "application/json": components["schemas"]["ErrorEnvelope"];
3606
+ };
3607
+ };
3608
+ /** @description Not signed in. */
3609
+ 401: {
3610
+ headers: {
3611
+ [name: string]: unknown;
3612
+ };
3613
+ content: {
3614
+ "application/json": components["schemas"]["ErrorEnvelope"];
3615
+ };
3616
+ };
3617
+ /** @description No organization on the session, or viewing the app as someone else. */
3618
+ 403: {
3619
+ headers: {
3620
+ [name: string]: unknown;
3621
+ };
3622
+ content: {
3623
+ "application/json": components["schemas"]["ErrorEnvelope"];
3624
+ };
3625
+ };
3626
+ /** @description Client major below minClientVersion. */
3627
+ 426: {
3628
+ headers: {
3629
+ [name: string]: unknown;
3630
+ };
3631
+ content: {
3632
+ "application/json": components["schemas"]["UpgradeRequiredEnvelope"];
3633
+ };
3634
+ };
3635
+ /** @description Too many requests from this person; try again later. */
3636
+ 429: {
3637
+ headers: {
3638
+ [name: string]: unknown;
3639
+ };
3640
+ content: {
3641
+ "application/json": components["schemas"]["ErrorEnvelope"];
3642
+ };
3643
+ };
3644
+ };
3645
+ };
3646
+ delete?: never;
3647
+ options?: never;
3648
+ head?: never;
3649
+ patch?: never;
3650
+ trace?: never;
3651
+ };
3398
3652
  }
3399
3653
  export type webhooks = Record<string, never>;
3400
3654
  export interface components {
@@ -3419,7 +3673,7 @@ export interface components {
3419
3673
  };
3420
3674
  /** @description Cross-platform version handshake manifest. See docs/api-versioning.md. */
3421
3675
  VersionManifest: {
3422
- /** @example 1.2.418 */
3676
+ /** @example 1.2.420 */
3423
3677
  apiVersion: string;
3424
3678
  /** @example 1.0.0 */
3425
3679
  minClientVersion: string;
@@ -4019,6 +4273,39 @@ export interface components {
4019
4273
  RealtimeWebrtcRequest: {
4020
4274
  sdp: string;
4021
4275
  };
4276
+ /** @description A tap whose time the office is asked to look at, with both times. It waits in Team › Needs attention. */
4277
+ PunchTimeCheck: {
4278
+ /** @description server_time: the phone's time was more than 2 minutes from the server's, and the server's was recorded. unverified: the phone restarted before sending, so the time couldn't be checked; the phone's was kept. clock_changed: the phone's clock was changed by hand. Late stops (T6): too_old: a stop more than 12 hours old that couldn't be proven was recorded at the 12-hour limit. replaced_auto_close: a proven stop replaced the automatic close (autoAt). after_auto_close: a stop later than the automatic close; the automatic close was kept. auto_close_kept: a stop earlier than the automatic close that wasn't applied (keptBecause). */
4279
+ reasons: ("server_time" | "unverified" | "clock_changed" | "too_old" | "replaced_auto_close" | "after_auto_close" | "auto_close_kept")[];
4280
+ /**
4281
+ * Format: date-time
4282
+ * @description The phone's own time for the tap.
4283
+ */
4284
+ phoneAt: string | null;
4285
+ /**
4286
+ * Format: date-time
4287
+ * @description The tap by the server's clock (arrival minus tapElapsedMs); null when the phone couldn't say.
4288
+ */
4289
+ serverAt: string | null;
4290
+ /**
4291
+ * Format: date-time
4292
+ * @description When the request reached the server.
4293
+ */
4294
+ receivedAt: string;
4295
+ tapElapsedMs: number | null;
4296
+ /** @description True when the phone's stopwatch proved the tap's time. */
4297
+ verified?: boolean;
4298
+ /**
4299
+ * Format: date-time
4300
+ * @description The automatic close a late stop replaced or was held against.
4301
+ */
4302
+ autoAt?: string;
4303
+ /**
4304
+ * @description Why a late stop wasn't applied: its time couldn't be proven, the office had already changed or approved the time, or the pay period is locked.
4305
+ * @enum {string}
4306
+ */
4307
+ keptBecause?: "unverified" | "office" | "locked";
4308
+ };
4022
4309
  /** @description One stretch of time at one site, inside a work day. durationMinutes stays gross; paid time is gross minus breakMinutes. */
4023
4310
  TimeEntry: {
4024
4311
  id: number;
@@ -4063,11 +4350,25 @@ export interface components {
4063
4350
  staleWarnedAt: string | null;
4064
4351
  /** @description Unpaid break minutes recorded against this entry. */
4065
4352
  breakMinutes: number;
4353
+ /**
4354
+ * @description Job time started by hand away from the job ('away') or with no usable location ('no_location'); null for a normal start. The office decides it.
4355
+ * @enum {string|null}
4356
+ */
4357
+ startCheck?: "away" | "no_location" | null;
4358
+ /** @description How far from the job's site center the start was, when startCheck is 'away'. */
4359
+ startDistanceM?: number | null;
4360
+ /**
4361
+ * @description The office's decision on a flagged start; null while it waits. 'not_approved' stays on the timesheet but off the job's costing.
4362
+ * @enum {string|null}
4363
+ */
4364
+ startReview?: "approved" | "not_approved" | "moved" | null;
4365
+ clockInCheck?: components["schemas"]["PunchTimeCheck"] & (Record<string, never> | null);
4366
+ clockOutCheck?: components["schemas"]["PunchTimeCheck"] & (Record<string, never> | null);
4066
4367
  /** Format: date-time */
4067
4368
  createdAt: string | null;
4068
4369
  /** @description Enrichment on /active and /entries. 'Office' for an office entry. */
4069
4370
  jobName?: string;
4070
- /** @description Enrichment on /entries and /active?scope=team. */
4371
+ /** @description Enrichment on /entries. */
4071
4372
  userName?: string;
4072
4373
  };
4073
4374
  /** @description Where a day's minutes went (shared/workDays.ts splitDayMinutes). */
@@ -4125,6 +4426,8 @@ export interface components {
4125
4426
  nearestJob?: unknown;
4126
4427
  } | null;
4127
4428
  notes: string | null;
4429
+ clockInCheck?: components["schemas"]["PunchTimeCheck"] & (Record<string, never> | null);
4430
+ clockOutCheck?: components["schemas"]["PunchTimeCheck"] & (Record<string, never> | null);
4128
4431
  editedBy: string | null;
4129
4432
  /** Format: date-time */
4130
4433
  editedAt: string | null;
@@ -4156,6 +4459,16 @@ export interface components {
4156
4459
  };
4157
4460
  /** @description Everything the phone needs to draw the day card and decide what it may record. */
4158
4461
  MyDayResponse: {
4462
+ /** @description The company's time tracking (payroll) add-on. False: no Clock In/Out, no work days, schedules, time cards or pay; job time still runs. Then open is null, today is empty, shiftEnd is null, payroll.breakButton and locationTrail are false, and the payroll routes answer 403 PAYROLL_OFF. Not the same as payroll.enabled. */
4463
+ payrollAddon: boolean;
4464
+ /** @description The company's pay rules as they touch this person. */
4465
+ payroll: {
4466
+ /** @description A pay period is set (the company's pay rules are on). */
4467
+ enabled: boolean;
4468
+ /** @description Show the break button (false when this person's lunch comes off automatically). */
4469
+ breakButton: boolean;
4470
+ breaksPaid: boolean;
4471
+ };
4159
4472
  /** @description True once this person is on the day model. */
4160
4473
  dayModel: boolean;
4161
4474
  open: components["schemas"]["WorkDay"] & (Record<string, never> | null);
@@ -4203,7 +4516,7 @@ export interface components {
4203
4516
  /** Format: date-time */
4204
4517
  stageChangedAt: string | null;
4205
4518
  };
4206
- /** @description One fence crossing as the device saw it. */
4519
+ /** @description One fence crossing as the device saw it. With tapElapsedMs (the time since the crossing), the server checks occurredAt against its own clock before the resolver uses it, and the entry the crossing opens or closes carries the check. A departure it proves isn't held to the 12-hour limit (T6). */
4207
4520
  ClockCrossing: {
4208
4521
  /** @description Client-generated UUID. Re-delivery is a no-op (unique index), so retry freely. */
4209
4522
  clientEventId: string;
@@ -4227,6 +4540,12 @@ export interface components {
4227
4540
  device?: "ios" | "web" | "android" | null;
4228
4541
  /** @description The user's auto-clock toggle at crossing time. False → the crossing is logged but never acted on. */
4229
4542
  autoClockEnabled?: boolean;
4543
+ /** @description Milliseconds from the tap to this send, from the phone's monotonic clock (a stopwatch the person can't change). An offline replay sends the time since the tap. The server's tap time is its own clock when the request arrives minus this. Within 2 minutes of the phone's time, the phone's time stands (never later than the tap); further apart, the server's tap time is recorded and the office is asked to look. A Clock Out or job time stop it proves this way keeps its real time however old; without it, a stop older than 12 hours is recorded at the 12-hour limit and flagged (T6). Send it with every queued tap, clock-outs included. */
4544
+ tapElapsedMs?: number | null;
4545
+ /** @description True when the phone restarted between the tap and this send, so there is no stopwatch reading (send no tapElapsedMs). A tap more than 2 minutes old keeps the phone's time and is flagged for the office. */
4546
+ tapTimeUnverified?: boolean | null;
4547
+ /** @description True when the phone's clock was changed by hand since the phone last synced. The tap is flagged for the office. */
4548
+ deviceClockChanged?: boolean | null;
4230
4549
  };
4231
4550
  /** @description Always 202. The clock state to trust is `openEntries`, not the client's own guess. */
4232
4551
  CrossingsAck: {
@@ -4296,6 +4615,8 @@ export interface components {
4296
4615
  };
4297
4616
  /** @description True only for the primary account. */
4298
4617
  canEdit: boolean;
4618
+ /** @description The caller's own switch, set by the office (owner's O9). False: watch no job-site fences and use no location for job time; job time comes only from Start/Stop job time. The office fence, Clock In/Out and the company switches are unaffected. Absent (an older server): treat as true. */
4619
+ jobSiteTracking?: boolean;
4299
4620
  };
4300
4621
  /** @description Always 202. */
4301
4622
  TrailPingResponse: {
@@ -4374,6 +4695,8 @@ export interface components {
4374
4695
  * @description The org-local date, which is not necessarily the device's.
4375
4696
  */
4376
4697
  today: string;
4698
+ /** @description False when the company has the time tracking add-on off: no shifts, hours or time off come back, and nothing covers now. */
4699
+ payrollAddon: boolean;
4377
4700
  /** @description Keyed by org-local ISO date. */
4378
4701
  hours: {
4379
4702
  [key: string]: {
@@ -4410,6 +4733,32 @@ export interface components {
4410
4733
  /** @description The last few decided ones, newest first. */
4411
4734
  recentTimeOff: components["schemas"]["TimeOffRequestSummary"][];
4412
4735
  };
4736
+ /** @description One person on the clock (server/onTheClock.ts). Job time with no paid day behind it is left out: that person is at a job, not on the clock. */
4737
+ OnTheClockRow: {
4738
+ userId: string;
4739
+ userName: string;
4740
+ /** @description The open site entry; null between sites (a day with nothing open). */
4741
+ entryId: number | null;
4742
+ jobId: number | null;
4743
+ /**
4744
+ * @description 'none' between sites.
4745
+ * @enum {string}
4746
+ */
4747
+ siteType: "job" | "office" | "none";
4748
+ jobName: string | null;
4749
+ /**
4750
+ * Format: date-time
4751
+ * @description When the work day began, else when the entry did.
4752
+ */
4753
+ since: string;
4754
+ };
4755
+ /** @description The signed-in person's request to have their account deleted. pending: waiting for the company; completed: the company marked it done; cancelled: the company cancelled it (asking again starts a new one). */
4756
+ AccountDeletionRequest: {
4757
+ /** @enum {string} */
4758
+ status: "pending" | "completed" | "cancelled";
4759
+ /** Format: date-time */
4760
+ requestedAt: string;
4761
+ };
4413
4762
  };
4414
4763
  responses: never;
4415
4764
  parameters: never;