type Address { street1: String! street2: String! city: String state: String county: String zip: String! country: String! } input AddressFilter { street1: StringFilter street2: StringFilter city: StringFilter state: StringFilter county: StringFilter zip: StringFilter country: StringFilter } input AddressSort { street1: SortOrder street2: SortOrder city: SortOrder state: SortOrder county: SortOrder zip: SortOrder country: SortOrder } input AddressInput { street1: String street2: String city: String state: String country: String zip: String } """ Intent hint that scopes which schema types and operations are pre-loaded for the AI. Multiple intents can be provided — their schema slices are unioned. When in doubt, or when the question spans multiple domains, use ALL. """ enum AiIntent { """ Actions performed by office or admin staff *about* a job: scheduling, editing job metadata (title, category, customer, color, contract), assigning crews, managing line items, route planning, and calendar management. If someone is looking at a job board or dispatcher screen and wants to change something about a job, this is the right intent. """ DISPATCH """ Actions performed by a technician *on-site during a visit*: updating job status (open/close/skip), capturing before/after photos, filling out visit forms, starting/stopping timers, and clocking in or out. If someone is standing at a job site with a phone, this is the right intent. """ FIELD_WORK """Invoices, line items, and customer payments.""" INVOICING """Estimates / quotes, their line items, and lifecycle status.""" ESTIMATING """Customers and their properties.""" CUSTOMERS """Services catalog, item pricing, and packages.""" PRICING """Users, crews, time tracking, and clock-in.""" TEAM """ Aggregate report queries across events, invoices, estimates, payments, and SMS. """ REPORTING """ SMS and messaging — inbound/outbound conversations with customers, delivery status, and messaging reports. """ COMMUNICATIONS """ Loads the full cross-domain schema slice. Use this when intent is unknown, the question spans multiple domains, or you are unsure whether DISPATCH or FIELD_WORK (or any other intent) is the right fit. Prefer ALL over guessing wrong. """ ALL } enum AiResponseFormat { """Default. AI response is formatted as Markdown.""" MARKDOWN """Plain text, suitable for SMS, email body, or legacy views.""" PLAIN_TEXT } input AiContextObjectInput { objectType: ObjectType! objectIds: [SafeInt!] } input AiContextInput { objects: [AiContextObjectInput!] intents: [AiIntent!] dateRange: DateRangeInput sessionId: String responseFormat: AiResponseFormat metadata: JSON } input DateRangeInput { start: DateTime! end: DateTime! } enum AiMessageSenderType { USER ASSISTANT } type AiMessage { id: SafeInt! senderType: AiMessageSenderType! text: String! createdAt: DateTime! } input AiMessageSort { id: SortOrder senderType: SortOrder createdAt: SortOrder } type AiChat { id: SafeInt! messages(orderBy: [AiMessageSort!], take: SafeInt, skip: SafeInt): [AiMessage!]! createdAt: DateTime! updatedAt: DateTime! } input AiChatSort { id: SortOrder createdAt: SortOrder updatedAt: SortOrder } type ApiTrial { active: Boolean! trialStartedAt: DateTime trialEndsAt: Date daysRemaining: Int! } input ApiTrialFilter { trialStartedAt: DateTimeFilter trialEndsAt: DateTimeFilter isActive: Boolean } enum AppEventType { ADMIN_CREATED ADMIN_DELETED ADMIN_UPDATED COMPANY_CREATED COMPANY_UPDATED CUSTOMER_ADDED_RATING CUSTOMER_CREATED CUSTOMER_CREDIT_ADJUSTED CUSTOMER_MARKED_INACTIVE CUSTOMER_PAYMENT_DECLINED @deprecated(reason: "Use PAYMENT_DECLINED") CUSTOMER_PAYMENT_METHOD_ADDED CUSTOMER_PAYMENT_METHOD_REMOVED CUSTOMER_PAYMENT_RECEIVED @deprecated(reason: "Use PAYMENT_RECEIVED") CUSTOMER_SMS_RECEIVED CUSTOMER_STATUS_UPDATED CUSTOMER_UPDATED ESTIMATE_ACCEPTED ESTIMATE_CHANGES_REQUESTED ESTIMATE_CREATED ESTIMATE_DECLINED ESTIMATE_DELETED ESTIMATE_DEPOSIT_RECEIVED ESTIMATE_DRAFTED ESTIMATE_INVOICED ESTIMATE_LINE_ADDED ESTIMATE_LINE_CREATED ESTIMATE_LINE_DELETED ESTIMATE_LINE_REMOVED ESTIMATE_LINE_UPDATED ESTIMATE_NOT_RESPONDED ESTIMATE_PENDING ESTIMATE_RESPONDED ESTIMATE_SENT ESTIMATE_SERVICE ESTIMATE_UPDATED ESTIMATE_VIEWED EVENT_ASSIGNED EVENT_CLOSED EVENT_COMPLETED EVENT_CREATED EVENT_DELETED EVENT_OPENED EVENT_SKIPPED EVENT_TIMER_STARTED EVENT_TIMER_STOPPED EVENT_UPDATED INVOICE_CREATED INVOICE_DELETED INVOICE_SENT INVOICE_UPDATED INVOICE_VIEWED MCP_QUERY_EXECUTED PAYMENT_DECLINED PAYMENT_RECEIVED RATING_CREATED ROUTE_CREATED SUBSCRIPTION_CANCELLED SUBSCRIPTION_CREATED SUBSCRIPTION_DOWNGRADED SUBSCRIPTION_UNCANCELLED SUBSCRIPTION_UPGRADED TAG_ADDED TAG_REMOVED TIME_ENTRY_CREATED TIME_ENTRY_DELETED TIME_ENTRY_UPDATED USER_CREATED USER_DELETED USER_IMPERSONATED USER_UPDATED WORK_REQUEST_CREATED } enum AppEventActorType { ADMIN CUSTOMER SYSTEM USER } type AppEventActor { id: SafeInt name: String email: String } input SignUpInput { email: String! password: String! recaptchaToken: String! } type SignUpResult { userId: SafeInt! companyId: SafeInt! accessToken: String! refreshToken: String! tokenType: String! expiresIn: Int! } type AuthResponse { accessToken: String! refreshToken: String tokenType: String! expiresIn: Int } enum RegistrationSource { GOOGLE PODCAST REFERRAL SEARCH SOCIAL_MEDIA YOUTUBE_MIKE_ANDES YOUTUBE_HOME_SERVICE_MILLIONAIRE INSTAGRAM FACEBOOK EMAIL FREE_COURSES REFERRAL_WORD_OF_MOUTH OTHER } input UserContactInput { firstName: String! lastName: String! } input CompanyContactInput { name: String! website: String phone: String source: RegistrationSource sourceOther: String sourceStimulus: String street1: String! street2: String city: String! state: String! zip: String! country: String! latitude: Float longitude: Float } type Query { """ Checks whether a user with the given email address already exists. Public endpoint — no authentication required. """ userExists(email: String!): Boolean! """Health check query. Returns 'pong' to confirm the API is reachable.""" ping: String! """ Returns the Stripe configuration and subscription data for the current company. """ stripe: Stripe """ The authenticated company's subscription, tier, and current billing period usage. """ subscription: CompanySubscription aiChat(id: SafeInt!): AiChat! aiChats(orderBy: [AiChatSort!], take: SafeInt = 1000, skip: SafeInt): [AiChat!]! """ Accessible to all authenticated company users. Returns the current user's company with its settings, plan, and billing information. """ company: Company! """ Returns all crews for the company. Defaults to non-deleted crews. Pass where: { isDeleted: true } to include deleted crews. Requires crews.READ. """ crews(where: CrewFilter = {isDeleted: false}, skip: SafeInt, take: SafeInt, orderBy: [CrewSort!]): [Crew] """ Returns a single crew by ID, including soft-deleted records. Throws NotFoundException if not found. Requires crews.READ. """ crew(crewId: SafeInt!): Crew! """ Aggregate customer data grouped by the dimensions you select (customerTypeId, sourceId, status, isDeleted, country, state, city, zip). If no dimension fields are selected, returns a single row with company-wide totals. Useful for geographic breakdowns, customer type distribution, lead source analysis, and acquisition trends. Requires customers.READ. """ customerReport(where: CustomerFilter, orderBy: [CustomerReportOrderBy!], take: SafeInt, skip: SafeInt): [CustomerReportRow!]! """ Returns all customers for the company. Defaults to non-deleted customers. Pass where: { isDeleted: true } to include deleted customers. Requires customers.READ. """ customers(where: CustomerFilter = {isDeleted: false}, skip: SafeInt, take: SafeInt = 1000, orderBy: [CustomerSort!]): [Customer!]! """ Returns a single customer by ID, including soft-deleted records. Throws NotFoundException if not found. Requires customers.READ. """ customer(customerId: SafeInt!): Customer! """ Returns all customer types available to this company — includes both company-specific types and global/system types (companyId = null). Requires customers.READ. """ customerTypes: [CustomerType!]! """ Returns all customer sources available to this company — includes both company-specific sources and global/system sources (companyId = null). Only returns visible sources. Requires customers.READ. """ customerSources: [CustomerSource!]! """ Returns grouped and aggregated reporting stats for estimates. Requires estimates.READ. """ estimateReport(where: EstimateFilter, orderBy: [EstimateReportOrderBy!], take: SafeInt, skip: SafeInt): [EstimateReportRow!]! """ Returns all estimates for the company. Defaults to non-archived, non-deleted estimates. Pass where: { isArchived: true } or { isDeleted: true } to override. Requires estimates.READ. """ estimates(where: EstimateFilter = {isArchived: false, isDeleted: false}, skip: SafeInt, take: SafeInt = 1000, orderBy: [EstimateSort!]): [Estimate!]! """ Returns a single estimate by ID, including soft-deleted records. Throws NotFoundException if not found. Requires estimates.READ. """ estimate(estimateId: SafeInt!): Estimate! """ Returns estimate line items, optionally filtered. Only line items belonging to non-archived, non-deleted estimates are returned. Requires estimates.READ. """ estimateLineItems(where: EstimateLineItemFilter, skip: SafeInt, take: SafeInt = 1000, orderBy: [EstimateLineItemSort!]): [EstimateLineItem!]! """ List event categories for the company. Excludes soft-deleted categories by default — pass where: { isDeleted: true } to include them. """ eventCategories(where: EventCategoryFilter, orderBy: [EventCategorySort!], take: SafeInt = 1000, skip: SafeInt): [EventCategory!]! """ Returns a single event category by ID. Returns null if not found or deleted. """ eventCategory(eventCategoryId: SafeInt!): EventCategory """ Accessible to all authenticated company users. Returns all forms attached to a specific event. """ eventForms(eventId: SafeInt!): [EventForm!]! """ Accessible to all authenticated company users. Returns a specific event form submission by ID. """ eventForm(eventFormId: SafeInt!): EventForm """ Returns events. Soft-deleted events are always excluded regardless of filter. If no filter is provided, a default date range of +/- 7 days UTC is applied. Users without canAccessAllEvents see only their own assigned events; users with canAccessAllEvents see all events. To include deleted events, pass isDeleted: true in the filter. """ events( """ If no where filter is provided, a default event date range of +/- 7 days UTC will be applied """ where: EventFilter orderBy: [EventSort!] take: SafeInt = 1000 skip: SafeInt ): [Event!]! """ Returns a single event by ID. Returns null if not found or soft-deleted. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). """ event(eventId: SafeInt!): Event """ Returns events for a specific user. Soft-deleted events are always excluded. If no where filter is provided, a default date range of +/- 7 days UTC is applied. Users without canAccessAllEvents are restricted to their own events regardless of the userId argument. """ eventsByUserId(userId: SafeInt!, where: EventFilter, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! """Deprecated. Use eventsByUserId instead.""" eventsByEmployeeUserId(userId: SafeInt!, where: EventWhere @deprecated, filter: EventFilter, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! @deprecated(reason: "Use eventsByUserId") """Returns grouped and aggregated reporting stats for events.""" eventReport(where: EventFilter, orderBy: [EventReportOrderBy!], take: SafeInt, skip: SafeInt): [EventReportRow!]! """ requires calendar.READ or higher. Returns all visible off-seasons defined for the company. """ offSeasons(where: OffSeasonFilter, orderBy: [OffSeasonSort!], take: Int, skip: Int): [OffSeason!]! """ requires calendar.READ or higher. Returns all recurring event series for the company. """ recurringEvents(where: RecurringEventFilter, orderBy: [RecurringEventSort!], take: Int, skip: Int): [RecurringEvent!]! """ requires calendar.READ or higher. Returns a single recurring event series by ID. """ recurringEvent(id: SafeInt!): RecurringEvent """ Returns grouped and aggregated reporting stats for invoices. Defaults to active, non-archived invoices. Override with e.g. `where: { deletedState: { equals: DELETED } }` to include soft-deleted invoices, or `where: { isArchived: true }` for archived ones. Requires invoices.READ. """ invoiceReport(where: InvoiceFilter, orderBy: [InvoiceReportOrderBy!], take: SafeInt, skip: SafeInt): [InvoiceReportRow!]! """ `take` defaults to 1000 but is not a hard cap — pass a higher value for larger result sets. Returns all invoices for the company. Defaults to non-archived, active invoices. Pass `where: { isArchived: true }` to include archived invoices, or `where: { deletedState: { equals: DELETED } }` to include soft-deleted invoices. Requires invoices.READ. """ invoices(where: InvoiceFilter = {isArchived: false, deletedState: {equals: ACTIVE}}, take: SafeInt = 1000, skip: SafeInt, orderBy: [InvoiceSort!]): [Invoice!]! """ Returns a single invoice by ID, including soft-deleted records. Throws NotFoundException if not found. Requires invoices.READ. """ invoice(invoiceId: SafeInt!): Invoice! """ Returns all items for the company. Defaults to non-deleted items. Pass where: { isDeleted: true } to include deleted items. Requires items.READ. """ items(where: ItemFilter = {isDeleted: false}, orderBy: [ItemSort!], take: SafeInt = 1000, skip: SafeInt): [Item!]! """ Returns all item packages for the company. Defaults to non-deleted packages. Pass where: { isDeleted: true } to include deleted packages. Requires items.READ. """ itemPackages(where: ItemPackageFilter = {isDeleted: false}, orderBy: [ItemPackageSort!], take: SafeInt = 1000, skip: SafeInt): [ItemPackage!]! """ Returns a single item by ID, including soft-deleted records. Throws NotFoundException if not found. Requires items.READ. """ item(itemId: SafeInt!): Item! """ Returns a single item package by ID, including soft-deleted records. Throws NotFoundException if not found. Requires items.READ. """ itemPackage(itemPackageId: SafeInt!): ItemPackage! """ Accessible to all authenticated company users. Returns all orders for the company. """ orders(where: OrderFilter, orderBy: [OrderSort!] = [{date: desc}, {id: desc}], take: SafeInt = 1000, skip: SafeInt): [Order!]! """ Returns a single order by ID. Throws NotFoundException if not found. Accessible to all authenticated company users. """ order(orderId: SafeInt!): Order! """ Returns grouped and aggregated reporting stats for payments. Requires payments.READ. """ paymentReport(where: PaymentFilter, orderBy: [PaymentReportOrderBy!], take: SafeInt, skip: SafeInt): [PaymentReportRow!]! """ Returns a paginated list of payments for the company. Requires payments.READ. """ payments(where: PaymentFilter, orderBy: [PaymentSort!], take: SafeInt, skip: SafeInt): [Payment!]! """ Returns a single payment by ID, including soft-deleted records. Throws NotFoundException if not found. Requires payments.READ. """ payment(paymentId: SafeInt!): Payment! """ Publicly accessible (no authentication required). Returns all available subscription plans with pricing and feature tiers. """ plans: [Plan!]! """Returns all properties for the company. Requires properties.READ.""" properties(where: PropertyFilter, orderBy: [PropertySort!], take: SafeInt = 1000, skip: SafeInt): [Property!]! """ Returns a single property by ID, including soft-deleted records. Throws NotFoundException if not found. Requires properties.READ. """ property(id: SafeInt!): Property! """ Aggregate route data grouped by the dimensions you select (crewId, equipmentId, date). If no dimension fields are selected, returns a single row with company-wide totals. Useful for windshield time summaries, gas spend by vehicle, and stops-per-crew reporting. Requires calendar.UPDATE. """ routeReport(where: RouteFilter, orderBy: [RouteReportOrderBy!], take: SafeInt, skip: SafeInt): [RouteReportRow!]! """Returns a single route by ID. Requires calendar.UPDATE.""" route(routeId: SafeInt!): Route! """Returns routes for the company. Requires calendar.UPDATE.""" routes(where: RouteFilter, orderBy: [RouteSort!], take: SafeInt = 1000, skip: SafeInt): [Route!]! """ Get drive time and distance for an ordered list of waypoints. Uses traffic-aware routing when departAt is provided. Results are cached for 1 week. """ directions( waypoints: [CoordinateInput!]! """ISO 8601 departure datetime. Omit to use non-traffic routing.""" departAt: DateTime """Cost per gallon for fuel cost estimate.""" costPerGallon: Float """Miles per gallon for fuel cost estimate.""" milesPerGallon: Float ): DirectionsResult! """ Returns the given events in optimized order plus route metrics. Read-only — call `createRoute` with the returned event order to persist it. Every requested event must belong to the company; an unknown id is an error rather than a silently smaller route. Errors too if the optimizer cannot fit a stop, naming the stop and the reason. Distances and times come from a directions call, not from the optimizer, so the legs always sum to the total. Cached for a week per provider, keyed by the stops and the day and hour of departure. """ optimizedRoute( """Event IDs to optimize. Input order does not matter.""" eventIds: [SafeInt!]! """ ISO 8601 departure datetime. Defaults to earliest event start or 8am local. """ departAt: DateTime """Optional depot start location. Falls back to company route settings.""" startLocation: CoordinateInput """Optional depot end location. Falls back to company route settings.""" endLocation: CoordinateInput """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider ): OptimizeRouteResult! """ Returns grouped and aggregated reporting stats for SMS messages. Requires texts.READ. """ smsReport(where: SmsFilter, orderBy: [SmsReportOrderBy!], take: SafeInt, skip: SafeInt): [SmsReportRow!]! """Returns SMS messages for the company. Requires texts.READ permission.""" smsMessages(where: SmsFilter, orderBy: [SmsSort!], take: SafeInt = 100, skip: SafeInt): [Sms!]! """ Validates a promo code for the given plan and previews the resulting checkout total so the UI can show the discounted price before creating the subscription. Returns isValid:false (not an error) for blank, unknown, or inactive codes. Requires role.OWNER. """ validatePromoCode(tier: SubscriptionTier!, billingInterval: BillingInterval!, promoCode: String!): PromoCodeValidation! tags(where: TagFilter, orderBy: [TagOrderBy!], take: Int, skip: Int): [Tag!]! """ Accessible to users with timeTracking: READ_OWN or higher. Users without READ_ALL are scoped to their own entries. """ timeEntryReport(where: TimeEntryFilter, orderBy: [TimeEntryReportOrderBy!], take: SafeInt, skip: SafeInt): [TimeEntryReportRow!]! timeEntryCategories(orderBy: [TimeEntryCategorySort]): [TimeEntryCategory!]! @deprecated timeEntryCategoryByName(name: String!): TimeEntryCategory @deprecated """ Accessible to all users. By default results are scoped to the current user (timeTracking: READ_OWN). Users with READ_ALL or higher can query any user. """ timeEntriesByEmployeeUserId( userId: SafeInt! take: SafeInt = 1000 skip: SafeInt """ If no where filter is provided, a default event date range of +/- 7 days UTC will be applied """ where: TimeEntryFilter orderBy: [TimeEntrySort!] ): [TimeEntry!]! """ Accessible to all users. Throws ForbiddenException for entries belonging to other users unless the caller has timeTracking: READ_ALL or higher. Throws NotFoundException if no entry exists with the given ID. """ timeEntry(timeEntryId: SafeInt!): TimeEntry! """ Returns true if the current user is/was clocked in. A user is considered clocked in if they have an active timer or if they have a clock in event today that has not yet been followed by a clock out event. """ isClockedIn(at: DateTime): Boolean! """Return the currently running timer for the current user, if it exists.""" runningTimer(where: TimeEntryFilter): TimeEntry """ Return the time entry for the current user that is/was active at the given time, inclusive of running timers. """ activeTimeEntry( where: TimeEntryFilter """Timestamp that the timer was running, if in the past""" at: DateTime ): TimeEntry """ Accessible to all users. By default results are scoped to the current user's entries (timeTracking: READ_OWN). Users with READ_ALL or higher can see all entries. """ timeEntries(where: TimeEntryFilter, orderBy: [TimeEntrySort!], take: SafeInt = 100, skip: SafeInt): [TimeEntry!]! twilioVerifications: [TwilioVerification!] """ Accessible to all authenticated company users. Returns all upsells configured for the company. """ upsells(where: UpsellFilter, orderBy: [UpsellSort!], take: SafeInt, skip: SafeInt): [Upsell!]! """ Returns a single upsell by ID. Throws NotFoundException if not found. Accessible to all authenticated company users. """ upsell(upsellId: SafeInt!): Upsell! """Returns all day overrides for the company. Requires users.READ.""" userDayOverrides(where: UserDayOverrideFilter, orderBy: [UserDayOverrideSort!], take: SafeInt = 1000, skip: SafeInt): [UserDayOverride!]! """ Returns all employees for the company. Defaults to active (non-deleted) users. Pass where: { isDeleted: true } to include deleted users. Requires users.READ. """ employees(where: UserFilter = {isDeleted: false}, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User] @deprecated(reason: "Use 'users'") """ Returns a user by ID, or the currently authenticated user if no ID is provided. Examples: - Current user: user { id firstName lastName role } - Lookup by ID: user(userId: 42) { id firstName lastName } """ user(userId: SafeInt): User """Returns the currently authenticated user's profile.""" currentUser: User """ Returns a single user by ID regardless of deleted status. Throws NotFoundException if not found. """ userById(userId: SafeInt!): User @deprecated(reason: "Use user(userId:) for lookup by ID, or currentUser for the authenticated user") """ Returns all users for the company. Defaults to active (non-deleted) users. Pass where: { isDeleted: true } to include deleted users. Requires users.READ. """ users(where: UserFilter = {isDeleted: false}, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User!]! """ Returns all vendors for the company. Defaults to non-deleted vendors. Pass where: { isDeleted: true } to include deleted vendors. Requires vendors.READ. """ vendors(where: VendorFilter = {isDeleted: false}, orderBy: [VendorSort!], take: SafeInt = 1000, skip: SafeInt): [Vendor!]! """ Returns a single vendor by ID, including soft-deleted records. Throws NotFoundException if not found. Requires vendors.READ. """ vendor(vendorId: SafeInt!): Vendor! """ Look up tax rates for a US address. Returns component rates (county, city, state) and the combined total, all expressed as percentages (e.g. 8.25 for 8.25%). Requires PROPERTIES:READ permission. For the most accurate result supply the full street address along with city and stateCode — ZipTax can then resolve to the exact tax jurisdiction rather than relying solely on the ZIP code. """ lookupTaxRate( """ US ZIP code (5-digit or ZIP+4). Optional when city/state or full address can identify the jurisdiction. """ zip: String """ Existing property ID. Enables Redis caching only when the property belongs to the current company and matches the lookup address. """ propertyId: Int """ City name — improves accuracy when multiple jurisdictions share the ZIP """ city: String """County name — used as a secondary filter when multiple results match""" county: String """ Street address — enables ZipTax address-level lookup for maximum accuracy """ address: String """Two-letter US state code (e.g. TX)""" stateCode: String ): TaxRateLookupResult! } type Mutation { """ Registers a new company and user account. Public endpoint — no authentication required. """ signUp(input: SignUpInput!): SignUpResult! """ Updates contact information for the company and user. Restricted to ADMIN and OWNER roles. """ updateContactInfo(companyContact: CompanyContactInput!, userContact: UserContactInput!): Company! """Health check mutation. Returns 'pong' to confirm the API is reachable.""" pingMutation: String! """ Generates a pre-signed S3 upload URL for a temporary file. Returns the upload URL and file key. """ createUpload(contentType: String!, size: SafeInt!): TempFile! """ Send a message to the AI assistant and receive a response. Available on Enterprise plans only. Not available to MCP or OAuth API callers — direct browser/app sessions only. Omit `chatId` to start a new conversation. Pass the `id` from a prior `AiChat` response to continue an existing thread. `responseFormat` defaults to MARKDOWN. Pass PLAIN_TEXT for SMS/email contexts. `contextData` is an optional opaque JSON blob passed verbatim into the AI system prompt. Use it to supply page context (e.g. currently-viewed record IDs, date range, feature area) without schema churn. """ sendAiMessage(message: String!, chatId: SafeInt, responseFormat: AiResponseFormat, contextData: JSON): AiChat! """Creates a new crew for the company. Requires crews.CREATE.""" createCrew(input: CreateCrewInput!): Crew! """Updates an existing crew. Requires crews.UPDATE.""" updateCrew(crewId: SafeInt!, input: UpdateCrewInput!): Crew! """ Soft-deletes a crew (isDeleted = true). Open events retain the crew reference. Requires crews.DELETE. """ deleteCrew(crewId: SafeInt!): Boolean! """ Restores a soft-deleted crew (isDeleted = false). Requires crews.UPDATE. """ restoreCrew(crewId: SafeInt!): Crew! """ Replaces all members of a crew with the provided user list. Optionally reassigns open/waitlisted events to the new member list. Requires crews.UPDATE. """ setCrewMembers(crewId: SafeInt!, userIds: [SafeInt!]!, reassignOpenEvents: Boolean = false): Crew! """ Adds a single member to a crew. Optionally reassigns open/waitlisted events to the updated member list. Requires crews.UPDATE. """ addCrewMember(crewId: SafeInt!, userId: SafeInt!, reassignOpenEvents: Boolean = false): Crew! """ Removes a single member from a crew. Optionally reassigns open/waitlisted events to the updated member list. Requires crews.UPDATE. """ removeCrewMember(crewId: SafeInt!, userId: SafeInt!, reassignOpenEvents: Boolean = false): Crew! """Create a new customer for the company. Requires customers.CREATE.""" createCustomer(input: CustomerInput!): Customer! """Update an existing customer. Requires customers.UPDATE.""" updateCustomer(customerId: SafeInt!, input: CustomerInput!): Customer! """ Set the active or inactive status of a customer. WRITTEN_OFF and PENDING_APPROVAL are read-only states and will be rejected. Requires customers.UPDATE. """ setCustomerStatus(customerId: SafeInt!, status: CustomerStatus!): Customer! """ Add or subtract from a customer's credit balance. Positive amount adds credit, negative subtracts. The resulting balance cannot go below 0. Requires customers.UPDATE. """ adjustCustomerCredit(customerId: SafeInt!, amount: Decimal!, note: String): Customer! """Creates a new estimate. Requires estimates.CREATE.""" createEstimate(input: CreateEstimateInput): Estimate! """Updates an existing estimate. Requires estimates.UPDATE.""" updateEstimate(estimateId: SafeInt!, input: UpdateEstimateInput): Estimate! """Deletes an estimate. Requires estimates.DELETE.""" deleteEstimate(estimateId: SafeInt!): Estimate! """Accepts an estimate. Requires estimates.UPDATE.""" acceptEstimate(estimateId: SafeInt!): Estimate! """Sets an estimate's status to Draft. Requires estimates.UPDATE.""" setEstimateStatusToDraft(estimateId: SafeInt!): Estimate! """ Sets an estimate's status to Invoiced without creating an invoice record. Requires estimates.UPDATE. """ setEstimateStatusToInvoiced(estimateId: SafeInt!): Estimate! """ Duplicates an estimate with a new number, today's date, and draft status. Requires estimates.CREATE. """ copyEstimate(estimateId: SafeInt!): Estimate! """Restores a soft-deleted estimate. Requires estimates.UPDATE.""" restoreEstimate(estimateId: SafeInt!): Estimate! """Marks an estimate as sent via copied link. Requires estimates.UPDATE.""" markEstimateAsCopyLinkSent(estimateId: SafeInt!): Estimate! """ Creates an invoice from an estimate and marks the estimate as invoiced. Requires estimates.UPDATE. """ createInvoiceFromEstimate(estimateId: SafeInt!): Invoice! """ Toggles optional line item inclusion on an estimate. Requires estimates.UPDATE. """ toggleEstimateLineItem(estimateId: SafeInt!, lineItemId: SafeInt!, include: Boolean!): Estimate! """Sets an estimate's status to Pending. Requires estimates.UPDATE.""" setEstimateStatusToPending(estimateId: SafeInt!): Estimate! """ Sets an estimate's status to Changes Requested. Requires estimates.UPDATE. """ setEstimateStatusToChangesRequested(estimateId: SafeInt!, requestedChanges: String!): Estimate! """ Sets an estimate's status to Changes Updated. Requires estimates.UPDATE. """ setEstimateStatusToChangesUpdated(estimateId: SafeInt!): Estimate! """ Declines an estimate with a reason and optional comment. Requires estimates.UPDATE. """ declineEstimate(estimateId: SafeInt!, reason: EstimateActionReason!, comment: String): Estimate! """Accepts multiple estimates. Requires estimates.UPDATE.""" acceptEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Archives multiple estimates. Requires estimates.UPDATE.""" archiveEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Unarchives multiple estimates. Requires estimates.UPDATE.""" unarchiveEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Deletes multiple estimates. Requires estimates.DELETE.""" deleteEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Creates a line item on an estimate. Requires estimates.UPDATE.""" createEstimateLineItem(estimateId: SafeInt!, input: EstimateLineItemInput): Estimate! """Updates a line item on an estimate. Requires estimates.UPDATE.""" updateEstimateLineItem(estimateId: SafeInt!, lineItemId: SafeInt!, input: EstimateLineItemInput): Estimate! """ Sets the event status of an estimate line item. Requires estimates.UPDATE. """ setEstimateLineItemEventStatus(estimateId: SafeInt!, lineItemId: SafeInt!, eventStatus: EstimateLineItemEventStatus!, eventId: SafeInt): EstimateLineItem! """Deletes a line item from an estimate. Requires estimates.DELETE.""" deleteEstimateLineItem(estimateId: SafeInt!, lineItemId: SafeInt!): Estimate! """ Generate a PDF representing selected estimates as a table/list (list-as-shown). Returns URL to the generated PDF. Requires estimates.UPDATE. """ createEstimateListPdf(estimateIds: [SafeInt!]!): String! """ Generate a page-per-estimate PDF for selected estimates. Returns URL to the generated PDF. Requires estimates.UPDATE. """ createEstimatesPdf(estimateIds: [SafeInt!]!, style: EstimatePrintStyle = CLEAN): String! """ Send estimates via EMAIL, MAIL, or SMS. Returns per-estimate status (QUEUED, SENT, or ERROR) with actionable error details. Requires estimates.UPDATE. """ sendEstimates(estimateIds: [SafeInt!]!, sendMethod: EstimateSendMethod!): EstimateSendResults! """ Manually mark estimates as sent (or clear sent flags for NOT_SENT) without triggering actual send. Requires estimates.UPDATE. """ markEstimatesAsSent(estimateIds: [SafeInt!]!, sentByType: EstimateSentByType!): [Estimate!]! """ Decline estimates with the same reason and optional comment for all. Requires estimates.UPDATE. """ declineEstimates(estimateIds: [SafeInt!]!, reason: EstimateActionReason!, comment: String): [Estimate!]! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ updateBeforeDescription(beforeAndAfterId: SafeInt!, description: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ updateAfterDescription(beforeAndAfterId: SafeInt!, description: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ addBeforeMedia(beforeAndAfterId: SafeInt!, tempFileName: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ addAfterMedia(beforeAndAfterId: SafeInt!, tempFileName: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ removeAfterMedia(beforeAndAfterId: SafeInt!, mediaId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ removeBeforeMedia(beforeAndAfterId: SafeInt!, mediaId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ removeBeforeAndAfter(beforeAndAfterId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ addBeforeAndAfter(eventId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ updateIsClientVisible(beforeAndAfterId: SafeInt!, isClientVisible: Boolean!): BeforeAndAfter! """ Update form content and signature (handles both initial submission and updates). Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). """ updateEventForm(eventFormId: SafeInt!, content: String!, signature: String): EventForm! """ Creates a single (non-recurring) event. Requires calendar.CREATE. """ createEvent(input: CreateEventInput!): Event! """ Attaches an existing form to an event. Requires calendar.UPDATE. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). """ addEventForm(eventId: SafeInt!, formId: SafeInt!): Event! """ Removes a form attachment from an event. Requires calendar.UPDATE. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). """ removeEventForm(eventId: SafeInt!, formId: SafeInt!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.UPDATE. """ updateEventDescription(eventId: SafeInt!, description: String!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.UPDATE, requires calendar.COMPLETE_AND_INVOICE (for CLOSED status), or requires calendar.SKIP (for SKIPPED status). """ updateEventStatus(eventId: SafeInt!, status: EventStatus!, at: DateTime): Event! """ Accessible to all calendar users; users without requires calendar.ACCESS_ALL are scoped to their own assigned events. """ updateEventStatuses(eventIds: [SafeInt!]!, status: EventStatus!, at: DateTime): [Event!]! """ Marks an event as closed/complete. Requires calendar.COMPLETE_AND_INVOICE. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Optionally pass invoiceDateSource to control how invoice line item dates are stamped. """ closeEvent(eventId: SafeInt!, at: DateTime, invoiceDateSource: EventInvoiceDateSource): Event! """ Marks multiple events as closed/complete. Requires calendar.COMPLETE_AND_INVOICE. Accessible to users assigned to the events (or requires calendar.ACCESS_ALL). Optionally pass invoiceDateSource to control how invoice line item dates are stamped. """ closeEvents(eventIds: [SafeInt!]!, at: DateTime, invoiceDateSource: EventInvoiceDateSource): [Event!]! """ Reopens one or more closed, skipped, or cancelled events, returning them to OPEN status. Clears closedAt, skippedAt, skippedReason, completedByUserId, and invoiceId. Only CLOSED, SKIPPED, and CANCELLED events are affected — OPEN and WAITLISTED events are silently bypassed. Requires calendar.COMPLETE_AND_INVOICE. Accessible to users assigned to the events (or requires calendar.ACCESS_ALL). """ reopenEvents(eventIds: [SafeInt!]!): [Event!]! """ Reopens a single closed, skipped, or cancelled event. See reopenEvents for full semantics. """ reopenEvent(eventId: SafeInt!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.SKIP. """ skipEvent(eventId: SafeInt!, skippedReason: String!, at: DateTime): Event! """ Marks multiple events as skipped. Requires calendar.SKIP. Accessible to users assigned to the events (or requires calendar.ACCESS_ALL). """ skipEvents(eventIds: [SafeInt!]!, skippedReason: String!, at: DateTime): [Event!]! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). """ createEventDispatchNote(eventId: SafeInt!, input: DispatchNoteInput!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.SKIP. """ updateEventSkippedReason(eventId: SafeInt!, skippedReason: String!, at: DateTime): Event! """ Bulk-schedules multiple events to the same start date. Same rules as scheduleEvent — each event moves to OPEN if waitlisted, throws if closed/skipped/cancelled. Existing end dates are shifted by the same interval to preserve multi-day duration. Each event retains its own budgetedHours and startTime. Accessible to users assigned to each event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ scheduleEvents( eventIds: [SafeInt!]! """The new start date for all events, in the company's timezone""" startDate: LocalDate! ): [Event!]! """ Schedules an event. If the event is waitlisted, it will move to OPEN. If the event is closed, skipped, or cancelled, an error will be thrown. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.UPDATE. """ scheduleEvent( eventId: SafeInt! """The date/start date of the event in the company's timezone""" startDate: LocalDate! """Optional start time of the event in the company's timezone""" startTime: LocalTime """ Exact inclusive last active date for a multi-day event. Can only be provided if startTime is null. """ endDate: LocalDate """ Budgeted hours / duration of the event. This is used to calculate endTime if startTime is provided. If not provided, the existing value will be used. """ budgetedHours: Decimal ): Event! """ Rename an event. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventTitle(eventId: SafeInt!, title: String!): Event! """ Set or clear the category on an event (e.g. todo/meeting type). Pass null to unset. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventCategory(eventId: SafeInt!, categoryId: SafeInt): Event! """ Change the property an event is scheduled at. Pass null to unlink. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventProperty(eventId: SafeInt!, propertyId: SafeInt): Event! """ Reassign an event to a different customer. Clears propertyId unless a new one is explicitly provided. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventCustomer(eventId: SafeInt!, customerId: SafeInt!, propertyId: SafeInt): Event! """ Link or unlink a contract on an event. Pass null contractId to unlink. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventContract(eventId: SafeInt!, contractId: SafeInt): Event! """ Set the color of an event. Pass an empty string to clear. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventColor(eventId: SafeInt!, color: String!): Event! """ Set whether a visit generates an invoice. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventInvoiceable(eventId: SafeInt!, isInvoiceable: Boolean!): Event! """ Add a line item to an event. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ createEventLineItem(eventId: SafeInt!, input: EventLineItemInput!): Event! """ Update a line item on an event. All fields are optional — omitted fields are left unchanged. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventLineItem(eventId: SafeInt!, lineItemId: SafeInt!, input: UpdateEventLineItemInput!): Event! """ Remove a line item from an event. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ deleteEventLineItem(eventId: SafeInt!, lineItemId: SafeInt!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Add media (photo or video) to an event. """ addEventMedia(eventId: SafeInt!, tempFileName: String!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Remove media from an event. """ removeEventMedia(eventId: SafeInt!, mediaId: SafeInt!): Event! """Hard-delete events by IDs. Requires calendar.DELETE.""" deleteEvents(eventIds: [SafeInt!]!): [SafeInt!]! """ Creates standalone copies of one or more events. Copies core fields, line items, user assignments, location, tags, visit photos, and visit forms. The recurringEventId is never copied — copied events are always standalone. Requires calendar.CREATE. """ copyEvents(eventIds: [SafeInt!]!): [Event!]! """ Assigns a crew to multiple events. Sets crewId AND replaces event_employees with the crew's current members. Pass crewId: null to clear both. Requires calendar.UPDATE. """ assignEventsCrew(eventIds: [SafeInt!]!, crewId: SafeInt): [Event!]! """ Assigns a crew to a single event. Sets crewId AND replaces event_employees with the crew's current members. Pass crewId: null to clear both. Requires calendar.UPDATE. """ assignEventCrew(eventId: SafeInt!, crewId: SafeInt): Event! """ Replaces employee assignments on multiple events. Clears crewId since the event is now individually staffed. userIds: [] removes all. Requires calendar.UPDATE. """ assignEventsUsers(eventIds: [SafeInt!]!, userIds: [SafeInt!]!): [Event!]! """ Replaces employee assignments on a single event. Clears crewId since the event is now individually staffed. userIds: [] removes all. Requires calendar.UPDATE. """ assignEventUsers(eventId: SafeInt!, userIds: [SafeInt!]!): Event! addEventTag(eventId: SafeInt!, tag: String!): Event! removeEventTag(eventId: SafeInt!, tag: String!): Event! addRecurringEventTag(recurringEventId: SafeInt!, tag: String!): RecurringEvent! removeRecurringEventTag(recurringEventId: SafeInt!, tag: String!): RecurringEvent! """ Generates a multi-event PDF for the given events and returns the PHP download URL. The caller opens the URL directly in their browser (shared http-only auth cookie). Requires calendar.ACCESS_ALL, or the user must be assigned to all requested events. """ createEventsPdf( eventIds: [SafeInt!]! """ When true, line item prices are included in the PDF. Defaults to false. """ showPrices: Boolean ): String! """ Creates a new invoice for the given customer. Invoice number, totals, global line items, and terms/notes are all resolved server-side. Requires invoices.CREATE. """ createInvoice(input: CreateInvoiceInput!): Invoice! """ Promote a temp S3 upload and attach it to the invoice. Requires invoices.UPDATE. """ addInvoiceMedia(invoiceId: SafeInt!, tempFileName: String!): Invoice! """ Remove media from an invoice (and delete S3 object when stored). Requires invoices.UPDATE. """ removeInvoiceMedia(invoiceId: SafeInt!, mediaId: SafeInt!): Invoice! """ Mark invoices as sent and queue delivery via EMAIL, SMS, or SNAIL_MAIL (PHP handles delivery via INVOICE_SENT). Requires invoices.UPDATE. """ sendInvoices(invoiceIds: [SafeInt!]!, sendMethod: InvoiceSendMethod!): [Invoice!]! """ Manually mark invoices as sent without triggering actual delivery. Requires invoices.UPDATE. """ markInvoicesAsSent(invoiceIds: [SafeInt!]!, sendMethod: InvoiceSendMethod!): [Invoice!]! """Write off one or more invoices. Requires invoices.UPDATE.""" writeOffInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Archive one or more invoices. Requires invoices.UPDATE.""" archiveInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Unarchive one or more invoices. Requires invoices.UPDATE.""" unarchiveInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Soft-delete a single invoice. Requires invoices.DELETE.""" deleteInvoice(invoiceId: SafeInt!): Invoice! """Soft-delete multiple invoices. Requires invoices.DELETE.""" deleteInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Restore a soft-deleted invoice. Requires invoices.UPDATE.""" restoreInvoice(invoiceId: SafeInt!): Invoice! """ Duplicate an invoice with a fresh number and draft status. Requires invoices.CREATE. """ copyInvoice(invoiceId: SafeInt!): Invoice! """Generate a PDF download URL for an invoice. Requires invoices.READ.""" createInvoicePdf(invoiceId: SafeInt!): String! """Add a line item to an invoice. Requires invoices.UPDATE.""" createInvoiceLineItem(invoiceId: SafeInt!, input: CreateInvoiceLineItemInput!): Invoice! """Update a line item on an invoice. Requires invoices.UPDATE.""" updateInvoiceLineItem(invoiceId: SafeInt!, lineItemId: SafeInt!, input: UpdateInvoiceLineItemInput!): Invoice! """Delete a line item from an invoice. Requires invoices.DELETE.""" deleteInvoiceLineItem(invoiceId: SafeInt!, lineItemId: SafeInt!): Invoice! """Sets isDeleted to true for the given Items""" deleteItems(itemIds: [SafeInt!]!): Boolean! """Sets isDeleted to false for the given Items""" restoreItems(itemIds: [SafeInt!]!): Boolean! adjustItemPrices( itemIds: [SafeInt!]! """Amount to adjust the price (negative is decrease)""" amount: Decimal! """Whether amount is a percentage (vs fixed) adjustment""" isPercent: Boolean = false """Whether to also update the unit price for open EventItems""" updateOpenEventItems: Boolean = false ): Boolean! createItem(input: ItemInput!): Item! updateItem(itemId: SafeInt!, input: ItemUpdateInput!, updateOpenVisits: Boolean): Item! """ Cancels a payment, issuing a Stripe refund if applicable. Requires payments.DELETE. """ deletePayment(paymentId: SafeInt!): Payment! """Cancels multiple payments in bulk. Requires payments.DELETE.""" deletePayments(paymentIds: [SafeInt!]!): [Payment!]! """ Creates a downloadable PDF report of payments by forwarding to the PHP PDF controller. Two modes: - Current-page export: supply paymentIds (the IDs visible on the current page). - Full date-range export: supply where/orderBy matching the current search filters. Returns a URL to the generated PDF file, or null when PDF generation fails. Requires payments.READ. """ createPaymentsPdf(paymentIds: [SafeInt!], where: PaymentFilter, orderBy: [PaymentSort!]): String """Create a new property for a customer. Requires properties.CREATE.""" createProperty(input: PropertyInput!): Property! """Update an existing property. Requires properties.UPDATE.""" updateProperty(propertyId: SafeInt!, input: UpdatePropertyInput!): Property! """Add media (photo or video) to a property. Requires properties.UPDATE.""" addPropertyMedia(propertyId: SafeInt!, tempFileName: String!): Property! """Remove media from a property. Requires properties.UPDATE.""" removePropertyMedia(propertyId: SafeInt!, mediaId: SafeInt!): Property! """ Creates a new route for a given date and set of events. If repeatEveryWeeks is set (> 0), the route is propagated to the same day of week every N weeks up to week 52, and all created routes share a series key. Future instances are only created for events that are themselves recurring instances; a route built from one-off events gets the series key but no future routes. One-off routes (repeatEveryWeeks omitted or 0) get seriesKey: null. Stops for the given eventIds are displaced from any other routes on the same date. Routes that become empty as a result are deleted. Requires calendar.UPDATE. """ createRoute( input: CreateRouteInput! """ Number of weeks between recurrences. E.g. 2 = every 2 weeks on the same day of week, up to week 52. Omit for a one-off route. """ repeatEveryWeeks: Int """ If true, the configured optimizer reorders stops before saving and eagerly populates directions fields. """ optimize: Boolean """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider ): Route! """ Updates an existing route by ID. The date is immutable — only the stop list and settings can change. All supplied input fields replace the existing values; omitted fields keep their current values. If andFutureRoutesInSeries is true, the update is propagated to all future routes sharing the same series key. A new series key is generated and stamped on all affected routes (the edited route and all futures). Routes before the edit date keep the old series key. The route must belong to a series (non-null seriesKey) when andFutureRoutesInSeries is true — otherwise an error is returned. A one-off update (andFutureRoutesInSeries: false or omitted) clears the series key on the edited route only, detaching it from the series. Requires calendar.UPDATE. """ updateRoute( routeId: SafeInt! input: UpdateRouteInput! """ Number of weeks between recurrences. Omit to keep existing series membership. """ repeatEveryWeeks: Int """ If true, the configured optimizer reorders stops and eagerly populates directions fields. """ optimize: Boolean """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider """ If true, propagates this update to all future routes in the same series. Generates a new series key for all affected routes. Requires the route to have a non-null seriesKey. """ andFutureRoutesInSeries: Boolean ): Route! """ Deletes a route (and its stops). Idempotent — deleting an already-deleted or non-existent route returns deletedCount: 0 rather than erroring. If andFutureRoutesInSeries is true, also deletes every future route sharing the same series key. Requires calendar.UPDATE. """ deleteRoute( routeId: SafeInt! """If true, also deletes all future routes sharing the same series key.""" andFutureRoutesInSeries: Boolean ): DeleteRouteResult! """ Deprecated — use createRoute or updateRoute instead. Creates or replaces the route for a given date and set of events. Requires calendar.UPDATE. """ saveRoute(date: LocalDate!, eventIds: [SafeInt!]!, crewId: SafeInt, departAt: LocalTime, startLocation: CoordinateInput, endLocation: CoordinateInput, repeatEveryWeeks: Int): Route! @deprecated(reason: "Use createRoute or updateRoute") """ Removes route stops for the given events. Cleans up any routes that become empty. If repeatEveryWeeks is set, also removes stops for future recurring instances. If date is provided, only removes stops for that specific route date. Requires calendar.UPDATE. """ removeEventRoutes(eventIds: [SafeInt!]!, repeatEveryWeeks: Int, date: LocalDate): [Event!]! """ Removes a single event from all route stops on a specific date and cleans up empty routes. Use when an event is rescheduled away from a date (e.g. event update changes startDate). Requires calendar.UPDATE. """ removeEventRoute(eventId: SafeInt!, date: LocalDate!): Boolean """ Re-optimize an existing route in place, keeping its id. Reads the current stop list, re-orders it with the routing provider, and saves the new order with leg data already populated, so a following read makes no further provider call. Requires calendar.UPDATE. """ optimizeRoute( routeId: SafeInt! """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider ): Route! """ Step 1 of subscription setup: creates a Stripe SetupIntent for securely collecting a new payment method. Requires role.OWNER. """ createStripeCompanySetupIntent: CompanyStripeSetupIntent! """ Step 2 of subscription setup: attaches the payment method from a completed SetupIntent to the company's Stripe customer. Requires role.OWNER. """ syncStripeCompanySetupIntent(setupIntentId: String!, billingAddress: BillingAddressInput!, setAsDefault: Boolean = true): StripePaymentMethod! """ Step 3 of subscription setup: creates a new Stripe subscription for the company at the given tier and billing interval. Requires role.OWNER. """ createPlanSubscription(tier: SubscriptionTier!, billingInterval: BillingInterval!, promoCode: String): Company! """ Immediately upgrades the subscription tier, or schedules a downgrade to take effect at the end of the current billing period. Requires role.OWNER. """ updatePlanSubscription(tier: SubscriptionTier!, billingInterval: BillingInterval!): Company! """ Clears any pending scheduled plan downgrade, keeping the current tier active. Requires role.OWNER. """ clearPendingPlanDowngrade: Company! """ Schedules the subscription to cancel at the end of the current billing period. Requires role.OWNER. """ cancelPlanSubscription(feedback: CancellationFeedbackInput!): Company! """ Clears a scheduled cancellation and keeps the subscription active. Requires role.OWNER. """ clearPendingPlanCancellation: Company! """Retry payment for past-due subscription invoice""" retrySubscriptionPayment(paymentMethodId: String): RetryPaymentResult! """Generate test properties for a customer from a predefined dataset""" generateTestProperties(customerId: Int!, dataset: PropertyDataSet!): [Property!]! """ Generate test events for a customer (one for every property the customer has). If customerId is omitted, generates events for all customers in the company. """ generateTestEvents(customerId: Int, startDate: LocalDate): [Event!]! """ Users with EDIT_OWN can only add entries for themselves. Users with EDIT_ALL can add entries for any user. Requires time_tracking.EDIT_OWN or higher. """ addTimeEntry(input: TimeEntryInput!): TimeEntry! """ Users with EDIT_OWN can only update their own entries. Users with EDIT_ALL can update entries for any user. Requires time_tracking.EDIT_OWN or higher. """ updateTimeEntry(timeEntryId: SafeInt!, input: UpdateTimeEntryInput!): TimeEntry! """ Explicitly clocks the current user into work and starts a global timer. This will return NULL if the user is/was already clocked in. This will throw an error if the pro timer feature is not enabled for this company. """ clockIn( """Optional parameter for the global timer category""" categoryId: SafeInt @deprecated location: LocationInput """Timestamp that the clock in action happened, if in the past""" at: DateTime ): ClockInOutAction """ Explicitly clocks the current user out of work and stops any running global or event timers. This will return NULL if the user was not clocked in """ clockOut( location: LocationInput """Timestamp that the clock out action happened, if in the past""" at: DateTime ): ClockInOutAction """ Accessible to all users. Users can clock themselves in. Clocking in another user requires timeTracking: EDIT_ALL. This will return NULL if the user is/was already clocked in. This will throw an error if the pro timer feature is not enabled for this company. """ clockInUser( userId: SafeInt! """Optional parameter for the global timer category""" categoryId: SafeInt @deprecated location: LocationInput """Timestamp that the clock in action happened, if in the past""" at: DateTime ): ClockInOutAction """ Accessible to all users. Users can clock themselves out. Clocking out another user requires timeTracking: EDIT_ALL. This will return NULL if the user was not clocked in """ clockOutUser( userId: SafeInt! location: LocationInput """Timestamp that the clock out action happened, if in the past""" at: DateTime ): ClockInOutAction """ Starts a global timer for the current user. If the user has an event timer or a global timer for another category, it will be stopped. If there is already a timer for this category or the company requires clock in and the user is not clocked in, this will throw an error. """ startGlobalTimer( categoryId: SafeInt location: LocationInput """Timestamp that the timer start happened, if in the past""" at: DateTime ): TimeEntry! @deprecated """ Starts an event timer for the current user. If the user has a global timer or timer for another event, it will be stopped. If there is already a timer for this event or the company requires clock in and the user is not clocked in, this will throw an error. """ startEventTimer( eventId: SafeInt! customerId: SafeInt @deprecated propertyId: SafeInt @deprecated location: LocationInput """Timestamp that the timer start happened, if in the past""" at: DateTime ): TimeEntry! """ Stops any running timer(s) for the current user. If the user is clocked in, then the global timer will be automatically resumed and returned. """ stopTimer( location: LocationInput """Timestamp that the timer stop happened, if in the past""" at: DateTime ): TimeEntry """ Deletes a time entry. Users with EDIT_OWN can only delete their own entries. Users with EDIT_ALL can delete entries for any user. Cannot delete a running timer — stop it first. Requires timeTracking: EDIT_OWN or higher. """ deleteTimeEntry(timeEntryId: SafeInt!): Boolean! """ Deletes multiple time entries. Users with EDIT_OWN can only delete their own entries. Users with EDIT_ALL can delete entries for any user. Cannot delete running timers — stop them first. Requires timeTracking: EDIT_OWN or higher. """ deleteTimeEntries(timeEntryIds: [SafeInt!]!): Boolean! """This will create and continue a Compliance Inquiry.""" createTwilioComplianceInquiry(mockError: MockTwilioError): ComplianceInquiryResponse """ Submit an upsell for an event. Creates an upsell history record. If the upsell requires review, it will be marked as pending. Otherwise, SMS/email will be sent. """ submitUpsell(input: SubmitUpsellInput!): UpsellHistory! """ Creates or updates a user's capacity hours for a specific date. Use capacityHours: 0 to mark the user as unavailable. Requires users.UPDATE. """ overrideUserDayCapacity(userId: SafeInt!, date: Date!, capacityHours: Decimal!): UserDayOverride! """ Deletes a user's day capacity override by user and date. Requires users.UPDATE. """ deleteUserDayOverride(userId: SafeInt!, date: Date!): Boolean! """Creates a new user/employee. Requires users.CREATE.""" createUser(input: CreateUserInput!): User! """Deletes a user/employee. Requires users.DELETE.""" deleteUser(userId: SafeInt!): Boolean! """ Recalculates additional user seats based on current active users. Requires users.UPDATE. """ updateAdditionalUsersCount: Company! """Updates the schedule colors for the currently authenticated user.""" setScheduleColors(input: SetScheduleColorsInput!): User! } """ A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar Date """ A time string at UTC, such as 10:15:30Z, compliant with the `full-time` format outlined in section 5.6 of the RFC 3339profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar Time """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar DateTime """ A local date string (i.e., with no associated timezone) in `YYYY-MM-DD` format, e.g. `2020-01-01`. """ scalar LocalDate """ A local time string (i.e., with no associated timezone) in 24-hr `HH:mm[:ss[.SSS]]` format, e.g. `14:25` or `14:25:06` or `14:25:06.123`. """ scalar LocalTime """ A local date-time string (i.e., with no associated timezone) in `YYYY-MM-DDTHH:mm:ss` format, e.g. `2020-01-01T00:00:00`. """ scalar LocalDateTime """ The `BigInt` scalar type represents non-fractional signed whole numeric values. """ scalar BigInt """ The `SafeInt` scalar type represents non-fractional signed whole numeric values that are considered safe as defined by the ECMAScript specification. """ scalar SafeInt @specifiedBy(url: "https://www.ecma-international.org/ecma-262/#sec-number.issafeinteger") """The `Decimal` scalar type to represent precise numeric values.""" scalar Decimal """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") """ Generic sort order These are lowercase to match the Prisma enum names without needing to convert them in each resolver """ enum SortOrder { asc desc } """Print style for estimate PDFs: clean (no stub) or with payment stub.""" enum EstimatePrintStyle { CLEAN WITH_PAYMENT_STUB } """Generic filters for string fields""" input StringFilter { equals: String not: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String } """Generic filters for number fields""" input IntFilter { equals: SafeInt not: SafeInt in: [SafeInt!] notIn: [SafeInt!] lt: SafeInt lte: SafeInt gt: SafeInt gte: SafeInt } """Generic filters for decimal fields""" input DecimalFilter { equals: Decimal not: Decimal in: [Decimal!] notIn: [Decimal!] lt: Decimal lte: Decimal gt: Decimal gte: Decimal } """ Generic filters for calendar date fields (YYYY-MM-DD). Use this for fields backed by a DB DATE column — accepts Date scalar values (e.g. \\"2026-06-01\\") and rejects full ISO timestamps, preventing timezone-shift bugs. """ input DateFilter { equals: Date not: Date in: [Date!] notIn: [Date!] lt: Date lte: Date gt: Date gte: Date } """ Generic filters for datetime fields using ISO format YYYY-MM-DDTHH:MM:SSZ """ input DateTimeFilter { equals: DateTime not: DateTime in: [DateTime!] notIn: [DateTime!] lt: DateTime lte: DateTime gt: DateTime gte: DateTime } enum ObjectType { ADMIN COMPANY CREW CUSTOMER CUSTOMER_PAYMENT_METHOD EQUIPMENT ESTIMATE EVENT INVOICE ITEM PAYMENT PROPERTY RATING RECURRING_EVENT TIME_ENTRY UNKNOWN USER VENDOR WORK_REQUEST } enum DepositRequirement { NONE ALL PER_CUSTOMER } enum InvoiceGrouping { PER_VISIT BY_CUSTOMER BY_PROPERTY BY_PROPERTY_AND_ASSET } enum InvoiceDatePolicy { DATE_SENT LAST_SERVICE_DATE NO_CHANGE } type CustomNaming { customer: String customers: String invoice: String invoices: String estimate: String estimates: String employee: String employees: String crew: String crews: String itemsAndService: String itemsAndServices: String equipment: String equipments: String tax: String taxes: String } type Company { id: SafeInt! name: String! email: String! isBanned: Boolean! @deprecated(reason: "Use accountStatus field instead") isActive: Boolean @deprecated(reason: "Use accountStatus field instead") accountStatus: AccountStatus! """Subscription details (plan, status, billing lifecycle)""" subscription: CompanySubscription address: Address! """ The company's main address, geocoded as a coordinate pair. Null if not yet geocoded. """ location: Location zip: String! phone: String mobile: String tier: Tier! @deprecated(reason: "Use subscription.tier instead") paypalEmail: String! isPaypalEnabled: Boolean! fax: String timezone: String! dformat: String! @deprecated(reason: "use dateFormat") tformat: String! @deprecated(reason: "use timeFormat") dsep: String! @deprecated(reason: "use decimalSeparator") tsep: String! @deprecated(reason: "use thousandsSeparator") dateFormat: String! timeFormat: String! decimalSeparator: String! thousandsSeparator: String! currency: String latitude: String @deprecated(reason: "Use location instead") longitude: String @deprecated(reason: "Use location instead") logo: File! www: String! stripeCustomerId: String! showInvoiceStatus: SafeInt! hideInvoiceOutstandingBalance: SafeInt! invoiceDueDate: SafeInt! profilePic: String! facebook: String! twitter: String! instagram: String! googleReviewLink: String showTips: Boolean! showCustomFields: Boolean! """ Which days the company operates — affects event scheduling and recurrence """ workDays: CompanyWorkDays! """ What happens to recurring (and any other system-generated) events that fall on an off day (only relevant when workDays ≠ WORK_ALL_DAYS) """ workDaysAction: CompanyWorkDaysAction! depositRequirement: DepositRequirement! depositThreshold: Decimal! depositAmount: Decimal! depositPercentage: Boolean! paymentMethodRequired: Boolean! showAddCredit: Boolean! settings: CompanySettings customNaming: CustomNaming plan: Plan @deprecated(reason: "Use subscription.planPriceId instead") pendingPlan: Plan @deprecated(reason: "Use subscription.pendingPriceId instead") """Is the Stripe subscription cancelled by the user""" isPlanCancelled: Boolean! @deprecated(reason: "Use accountStatus field instead") """Whether the subscription payment failed or lapsed after cancellation""" isPlanExpired: Boolean! @deprecated(reason: "Use accountStatus field instead") """Date the current subscription expires (if cancelled) or renews""" planPeriodEndsAt: DateTime @deprecated(reason: "Use subscription.planPeriodEndsOn instead") stripeSubscription: JSON stripeSubscriptions: [JSON!]! """Date the subscription started""" registrationDate: DateTime! """Number of currently active users""" activeUsers: SafeInt! """Number of additional user seats purchased""" userSeatsPurchased: Int! """ Default payment method on the company's platform Stripe customer (Copilot billing). Same `StripePaymentMethod` shape as client-portal saved methods, singular. """ stripeDefaultPaymentMethod: StripePaymentMethod stamps: SafeInt! """Twilio/SMS configuration (read-only)""" twilio: Twilio! """QuickBooks status (read-only, null if not connected)""" quickbooks: QuickBooks taxId: String! abn: String! """ If company has not yet finished signup and filled out their contact information """ isFirst: Boolean! @deprecated(reason: "Use accountStatus field instead") """ When true, the company's main business address is used as both the route start depot and end depot, overriding routeStartLocation and routeEndLocation. When false (the default), routeStartLocation and routeEndLocation are used when set; the company's main address is the fallback when they are null. """ useCompanyAddressAsDepot: Boolean! """ Custom route start location (depot). Null when coords are not set (or are the default 0/0), or when useCompanyAddressAsDepot is true (in which case the company's main address is used instead). """ routeStartLocation: Location """ Custom route end location. Null when coords are not set, or when useCompanyAddressAsDepot is true. When null and useCompanyAddressAsDepot is false, route optimization falls back to routeStartLocation, then the company's main address. """ routeEndLocation: Location """ Fully resolved depot start location, applying the same fallback chain as the route engine (useCompanyAddressAsDepot → stored coords → geocode name → company main address). Null only if nothing is configured and geocoding fails. Use this (not routeStartLocation) to place the depot marker on a map. """ resolvedRouteStartLocation: Location """ Fully resolved depot end location. Falls back through the same chain as resolvedRouteStartLocation. Null only if nothing is configured and geocoding fails. """ resolvedRouteEndLocation: Location """Whether the Price Matrix feature is enabled for this company""" isPriceMatrixEnabled: Boolean! apiTrial: ApiTrial } type CompanySettings { id: SafeInt! invoiceNotes: String! invoiceTerms: String! estimateNotes: String! estimateTerms: String! weekStart: SafeInt! colorClose: String! colorVisit: String! colorTask: String! colorMeet: String! colorMaint: String! colorInvoice: String! invoiceGrouping: InvoiceGrouping! invoiceDueDate: String! invoicePastDueLateFee: String! invoiceTermsRed: Boolean! estimateTermsRed: Boolean! defaultInvoiceServiceDate: Boolean showLivechat: Boolean! showGettingStarted: Boolean! showWrsd: JSON hideWrf: Boolean! invoiceDatePolicy: InvoiceDatePolicy! eventInvoiceDateSource: EventInvoiceDateSource! enablePublicWrf: Boolean! workingHours: String! showEstimateBudgetedHours: Boolean showInvoiceBudgetedHours: Boolean showEstimateQuantityPerHour: Boolean showInvoiceQuantityPerHour: Boolean showEstimateCostRate: Boolean showInvoiceCostRate: Boolean } """QuickBooks status (read-only, null if not connected)""" type QuickBooks { isConnected: Boolean! invoicesLastDownloaded: DateTime invoicesSyncFailedCount: Int! customersLastDownloaded: DateTime customersSyncFailedCount: Int! paymentsLastDownloaded: DateTime paymentsSyncFailedCount: Int! expiredAt: DateTime } """ Controls which date is used on invoice line items when an event is invoiced. - SCHEDULED_DATE: use the event's scheduled date. - CLOSED_DATE: use the date the event was closed (closedAt) as the invoice line item date. - PROMPT_USER: ask the user to choose at close time (only shown when today ≠ scheduled date and the event is invoiceable). """ enum EventInvoiceDateSource { SCHEDULED_DATE CLOSED_DATE PROMPT_USER } """ What happens to events that fall on a company off day. Only relevant when workDays ≠ WORK_ALL_DAYS. """ enum CompanyWorkDaysAction { """Skip (do not create) the event if it falls on an off day""" SKIP """Move the event to the preceding Friday""" MOVE_TO_FRIDAY """Move the event to the following Monday""" MOVE_TO_MONDAY """Move to Friday if it falls on Saturday, Monday if it falls on Sunday""" MOVE_TO_FRIDAY_OR_MONDAY } """ Which days of the week the company operates. Controls whether events fall on off-days and how recurrences are scheduled. """ enum CompanyWorkDays { """Work all 7 days a week""" WORK_ALL_DAYS """No Saturdays, work Sundays""" NO_WORK_ON_SATURDAY """No Sundays, work Saturdays""" NO_WORK_ON_SUNDAY """No Saturdays or Sundays""" NO_WORK_WEEKEND } """ Account status - controls app access (ACTIVE, PAST_DUE, EXPIRED, CANCEL_REQUESTED, CANCELLED, BANNED, INACTIVE, SIGNUP_INCOMPLETE) """ enum AccountStatus { ACTIVE PAST_DUE EXPIRED CANCEL_REQUESTED CANCELLED BANNED INACTIVE SIGNUP_INCOMPLETE } """Twilio/SMS configuration (read-only)""" type Twilio { number: String! sms: Boolean! unlimitedTexts: Boolean periodStart: DateTime expiredAt: DateTime """Subaccount SID (admin); null if not set""" subAccountSid: String """Messaging service SID / A2P (admin); null if not set""" messagingServiceSid: String """ Whether an encrypted subaccount auth token is stored (token is never exposed) """ hasSubAccountToken: Boolean! status: TwilioStatus! } """Status of a company's Twilio SMS setup""" enum TwilioStatus { NOT_STARTED PENDING ACTIVE CANCELED } """Deprecated. Use Crew.users instead.""" type CrewMember { id: SafeInt! crewId: SafeInt! userId: SafeInt! user: User! } type Crew { id: SafeInt! name: String! description: String notes: String! lp4Id: String! isDeleted: Boolean! deletedAt: DateTime tags: [String!]! color: String eventCount(where: EventFilter): SafeInt! events(where: EventFilter = {isDeleted: false}, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!] users(where: UserFilter = {isDeleted: false}, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User!] """Deprecated. Use users instead.""" members(take: SafeInt = 1000, skip: SafeInt): [CrewMember!] @deprecated(reason: "Use users") } input CrewFilter { id: IntFilter name: StringFilter isDeleted: Boolean events: EventFilter AND: [CrewFilter!] OR: [CrewFilter!] NOT: CrewFilter } enum CustomFieldType { NONE DATE CHECKBOX DEC INT TEXT } type DynamicCustomField { id: SafeInt! name: String! type: CustomFieldType! } type CustomField { id: SafeInt! name: String type: CustomFieldType textValue: String! integerValue: SafeInt! decimalValue: Decimal! dateValue: Date dynamicCustomField: DynamicCustomField } """ Input for setting a custom field value on create/update. Exactly one value field should be set, matching the DynamicCustomField's type. """ input CustomFieldInput { """ID of the DynamicCustomField definition for this company.""" fieldId: SafeInt! textValue: String intValue: SafeInt decimalValue: Decimal dateValue: LocalDate } enum AutoChargePolicy { """ Default/unconfigured state — customer inherits company auto-charge settings. Functionally identical to USE_COMPANY at runtime; distinguished only by whether the setting was explicitly configured or simply never set. """ INHERIT """ Auto-charge explicitly disabled for this customer, regardless of company settings. """ DISABLED """ Explicitly set to follow company-level auto-charge settings. Functionally identical to INHERIT at runtime; distinguished only by whether the setting was explicitly configured or simply never set. """ USE_COMPANY } """Status of a customer account.""" enum CustomerStatus { INACTIVE ACTIVE WRITTEN_OFF PENDING_APPROVAL } input CustomerStatusFilter { equals: CustomerStatus not: CustomerStatus in: [CustomerStatus!] notIn: [CustomerStatus!] } type CustomerType { id: SafeInt! name: String! } type Customer { id: SafeInt! """Contact email""" email: String! ccEmail: String! ccEmail1: String! ccEmail2: String! ccEmail3: String! fullName: String! firstName: String! lastName: String! dateRegistered: DateTime address: Address! """ The customer's address, geocoded as a coordinate pair. Null if not yet geocoded. """ location: Location """Business or landline phone number.""" phone: String! fax: String! """ Mobile/cell phone number. This is the field to set for SMS — always populate this when SMS is needed. For US customers, this value is passed to Twilio directly. For international customers, SMS uses cellFull (which is auto-computed from this field + country). """ cell: String! customerType: CustomerType customerTypeId: SafeInt sourceId: SafeInt status: CustomerStatus! description: String! isDeleted: Boolean! discountPercent: Decimal! credit: Decimal! """Used for client portal URL""" portalKey: String! lastLogin: DateTime outstanding: Decimal! lifetimePaid: Decimal! pastDue: Decimal! pic: String! createdAt: DateTime """Timestamp of the last write to this customer record.""" updatedAt: DateTime """ QuickBooks sync tracking field — not a general-purpose update timestamp. Use updatedAt for incremental sync. """ quickbooksModifiedAt: DateTime """ @deprecated Use updatedAt for incremental sync. quickbooksModifiedAt for the QuickBooks-specific field. """ lastUpdated: DateTime @deprecated(reason: "QuickBooks sync field, not a reliable update timestamp. Use updatedAt for incremental sync.") """Website""" www: String! title: String! invoiceTerms: String! number: String! """ Read-only. Computed E.164-style number (digits only, no '+') derived from cell + country. Examples: '15551234567' (US), '61412345678' (AU). Used for outbound SMS to international customers and for inbound SMS matching. Do not set this directly — set cell (and ensure country is correct) and it is computed automatically. """ cellFull: String! tags: [String!]! portalNeedsLogin: Boolean! """Email address used for login""" loginEmail: String! """Whether customer receives invoices via phone""" isReceivePhone: Boolean! """Whether customer receives invoices via email""" isReceiveEmail: Boolean! """Whether customer receives invoices via text""" isReceiveText: Boolean! hasPaymentMethod: Boolean hideInvoiceOutstandingBalance: SafeInt! showTotalDue: Boolean showInvoiceStatus: SafeInt! """Days past due?""" pastDueTerms: SafeInt! pastDueValue: Decimal! pastDueValueType: String! invoiceDueDate: SafeInt! """Show customer's available credit""" showCreditAvailable: Boolean! autoCreditApply: Boolean! autoChargePolicy: AutoChargePolicy! autoChargeEnabledAt: DateTime showTips: Boolean! deletedAt: DateTime pastDueTermsConvertAt: DateTime pastDueTermsConvertType: String pastDueTermsSettingsArchive: String allowAch: Boolean! ratingRequestSentAt: DateTime """Toggled in the customer settings""" portalNeedsPaymentMethod: Boolean! """Derived field that takes into account company settings""" paymentMethodRequired: Boolean! depositThreshold: Decimal! depositAmount: Decimal! depositPercentage: Boolean! """Whether the customer has opted in to receive marketing emails""" receiveMarketingEmails: Boolean! """ Custom field values set for this customer. Seeded for every DynamicCustomField that applies to customers. """ customFields: [CustomField!]! properties(where: PropertyFilter, orderBy: [PropertySort!], take: SafeInt = 1000, skip: SafeInt): [Property!] source: CustomerSource eventCount(where: EventFilter): SafeInt! """Customer's payment history""" payments(where: PaymentFilter = {isDeleted: false}, orderBy: [PaymentSort!], take: SafeInt = 1000, skip: SafeInt): [Payment!]! stripePaymentMethods: [StripePaymentMethod!]! """ Stripe Sources (their legacy API for payment methods). This is currently the only way to know if a bank account is verified. """ stripePaymentSources(type: StripePaymentSourceType): [StripePaymentSource!]! """ Invoices for this customer. Defaults to non-archived, active invoices. Pass where: { isArchived: true } to include archived, or where: { deletedState: { equals: DELETED } } to include soft-deleted. """ invoices(where: InvoiceFilter = {isArchived: false, deletedState: {equals: ACTIVE}}, orderBy: [InvoiceSort!], take: SafeInt = 1000, skip: SafeInt): [Invoice!]! """ Estimates for this customer. Defaults to non-archived, non-deleted estimates. """ estimates(where: EstimateFilter = {isArchived: false, isDeleted: false}, orderBy: [EstimateSort!], take: SafeInt = 1000, skip: SafeInt): [Estimate!]! """ Events (jobs/visits) for this customer. Defaults to non-deleted events. """ events(where: EventFilter = {isDeleted: false}, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! } input TagsFilter { in: [String!] notIn: [String!] } input CustomerFilter { id: IntFilter email: StringFilter fullName: StringFilter firstName: StringFilter lastName: StringFilter dateRegistered: DateTimeFilter address: AddressFilter phone: StringFilter fax: StringFilter cell: StringFilter customerTypeId: IntFilter status: CustomerStatusFilter description: StringFilter tags: TagsFilter isDeleted: Boolean events: EventFilter """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter AND: [CustomerFilter!] OR: [CustomerFilter!] NOT: CustomerFilter } input CustomerSort { id: SortOrder firstName: SortOrder lastName: SortOrder fullName: SortOrder email: SortOrder updatedAt: SortOrder createdAt: SortOrder } type Document { id: SafeInt! companyId: SafeInt! description: String! text: String! date: DateTime! customerId: SafeInt name: String! isArchived: Boolean! sentEmail: Boolean! snailMail: Boolean! view: Boolean } """A piece of equipment or vehicle belonging to a company.""" type Equipment { id: SafeInt! companyId: SafeInt! crewId: SafeInt name: String type: Int status: Int } input EstimateReportOrderBy { status: SortOrder customerId: SortOrder date: SortOrder isSent: SortOrder _count: EstimateReportCountOrderBy _sum: EstimateReportSumOrderBy _avg: EstimateReportAvgOrderBy _min: EstimateReportMinOrderBy _max: EstimateReportMaxOrderBy count: EstimateReportCountOrderBy @deprecated(reason: "Use _count") sum: EstimateReportSumOrderBy @deprecated(reason: "Use _sum") avg: EstimateReportAvgOrderBy @deprecated(reason: "Use _avg") min: EstimateReportMinOrderBy @deprecated(reason: "Use _min") max: EstimateReportMaxOrderBy @deprecated(reason: "Use _max") } input EstimateReportCountOrderBy { id: SortOrder } input EstimateReportSumOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } input EstimateReportAvgOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } input EstimateReportMinOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } input EstimateReportMaxOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } type EstimateReportCountAggregate { id: SafeInt! } type EstimateReportSumAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } type EstimateReportAvgAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } type EstimateReportMinAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } type EstimateReportMaxAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } enum EstimateStatus { """Not yet sent to the customer""" DRAFT """Sent to the customer, awaiting their decision""" PENDING """Customer accepted — ready to schedule and invoice""" ACCEPTED """Customer declined the estimate""" DECLINED """An invoice has been created from this estimate""" INVOICED """Customer has requested changes (shown as 'Changes Requested' in UI)""" CHANGE_REQUESTED """ Changes have been applied in response to a change request (shown as 'Changes Updated' in UI) """ CHANGE_UPDATED } enum EstimateChangeRequested { NO_CHANGE_REQUESTED CHANGE_APPLIED CHANGE_REQUESTED } type Estimate { id: SafeInt! number: SafeInt! purchaseOrder: String! date: Date! status: EstimateStatus! title: String notes: String! terms: String! subtotal: Decimal! discountPercent: Decimal! discountAmount: Decimal! tax: Decimal! """The total cost of this estimate, including subtotal, discounts and tax""" total: Decimal! """The total less any previous payments for this estimate""" totalOwed: Decimal! """The sum of all deposit payments that have been made for this estimate""" paidAmount: Decimal! """The depositRequirement less any previous payments for this estimate""" depositOwed: Decimal! """ The required deposit in order to accept this estimate, based on company and customer settings """ depositRequirement: Decimal! payInFullEnabled: Boolean! isArchived: Boolean! hasBeenViewed: Boolean! requested: EstimateChangeRequested! requestedChanges: String createdAt: DateTime updatedAt: DateTime requestedAt: DateTime acceptedAt: DateTime lineItems(where: EstimateLineItemFilter, orderBy: [EstimateLineItemSort!] = [{sortOrder: asc}, {id: asc}]): [EstimateLineItem!]! media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! history(where: EstimateHistoryFilter, orderBy: [EstimateHistorySort!], take: SafeInt = 1000, skip: SafeInt): [EstimateHistory!]! customFields: [CustomField!]! customerId: SafeInt! customer: Customer! internalNotes: String isSent: Boolean! sentEmail: Boolean! sentSms: Boolean! sentSnailMail: Boolean! invoiceId: SafeInt! requestId: SafeInt invoiceNumber: SafeInt! isDeleted: Boolean! tags: String! emailSubject: String! emailMessage: String! droppedOffAtProperty: Boolean! emailOpened: Boolean! sentCopyLink: Boolean! budgetedHoursTotal: Decimal budgetedHoursCost: Decimal profit: Decimal visitCost: Decimal """ User ID of the estimator who created this estimate. Null when the creator was not recorded. """ createdByUserId: SafeInt """ User who created this estimate. Null when createdByUserId is null or the user no longer exists. Soft-deleted users still resolve for historical accuracy. """ createdByUser: User } input EstimateStatusFilter { equals: EstimateStatus not: EstimateStatus in: [EstimateStatus!] notIn: [EstimateStatus!] } input EstimateFilter { id: IntFilter estimateId: IntFilter number: IntFilter purchaseOrder: StringFilter propertyId: IntFilter date: DateFilter status: EstimateStatusFilter title: StringFilter notes: StringFilter terms: StringFilter isArchived: Boolean isDeleted: Boolean subtotal: DecimalFilter discountPercent: DecimalFilter discountAmount: DecimalFilter tax: DecimalFilter total: DecimalFilter hasBeenViewed: Boolean requestedChanges: StringFilter createdAt: DateTimeFilter updatedAt: DateTimeFilter requestedAt: DateTimeFilter """Has accepted line items that have not been scheduled""" hasUnscheduledLineItems: Boolean AND: [EstimateFilter!] OR: [EstimateFilter!] NOT: [EstimateFilter!] customerId: IntFilter customer: CustomerFilter internalNotes: StringFilter isSent: Boolean invoiceId: IntFilter sentCopyLink: Boolean sentEmail: Boolean sentSnailMail: Boolean sentSms: Boolean budgetedHoursTotal: DecimalFilter budgetedHoursCost: DecimalFilter acceptedAt: DateTimeFilter } enum EstimateLineItemRemoved { """is included on estimate""" RETAINED """customer or company user unchecked the line item""" REMOVED """line item is included on estimate""" RETAINED_AND_ACCEPTED } input EstimateLineItemRemovedFilter { equals: EstimateLineItemRemoved not: EstimateLineItemRemoved in: [EstimateLineItemRemoved!] notIn: [EstimateLineItemRemoved!] } type EstimateLineItem { id: SafeInt! eventId: SafeInt eventIsRecurring: Boolean! date: Date! eventItemId: SafeInt itemId: SafeInt name: String! description: String! """Sort order of the line item""" sortOrder: SafeInt! price: Decimal! cost: Decimal! @deprecated(reason: "Use price instead") quantity: Decimal! """ Estimated hours for this line item. Not matrix-specific — can be set on any line item. Per-visit value; for matrix lines, total hours across all visits = budgetedHours × visitCount. """ budgetedHours: Decimal taxPercent1: Decimal! taxPercent2: Decimal! taxPrice1: Decimal! taxPrice2: Decimal! subtotal: Decimal! subtotalWithTax: Decimal! subtotalWithDiscount: Decimal! propertyId: SafeInt property: Property isRecommended: Boolean! """Rename to isRemoved?""" removed: EstimateLineItemRemoved! """ Number of visits the rate matrix priced this line item for. Null/0 on non-matrix lines. """ visitCount: SafeInt! """ Authoritative customer-facing subtotal for matrix lines. Derived as visitCount × perVisitRate (mode 0) or visitCount × rm_rate × calc (mode 1). Per-visit price = priceAllVisits ÷ visitCount. Null/0 on non-matrix lines. """ priceAllVisits: Decimal! """ Internal company labour rate ($/hr) from the rate matrix — not customer-facing. Null/0 on non-matrix lines. """ internalHourlyRate: Decimal! """ The custom-field measurement value (e.g. square footage, lot size) used as input to the rate matrix lookup. Null on non-matrix lines. """ matrixCalcInput: String """ FK to the rate matrix config (price_matrices_items) that priced this line. Null = not a matrix line. """ matrixItemId: SafeInt estimateId: SafeInt! estimate: Estimate! item: Item """AKA scheduledStatus""" eventStatus: EstimateLineItemEventStatus! """ Finds the most efficient ways to insert this line item's property location into existing routes, grouped by day. Evaluates the startDate–endDate range (defaults to the line item's scheduled date). Returns an empty list if the line item has no property with valid coordinates. """ routeSuggestions( """ Start of the date range to evaluate (inclusive, YYYY-MM-DD). Defaults to the line item's scheduled date. """ startDate: LocalDate """ End of the date range to evaluate (inclusive, YYYY-MM-DD). Defaults to startDate. """ endDate: LocalDate """ Filter by crew, customer, or other event properties. Does not affect the date range. """ eventFilter: EventFilter """ If true, use crow-flies for all calculations. No directions API calls made. """ fastMode: Boolean ): [SchedulingDateSuggestion!]! } input EstimateLineItemFilter { id: IntFilter estimateId: IntFilter estimate: EstimateFilter date: DateFilter itemId: IntFilter item: ItemFilter name: StringFilter description: StringFilter price: DecimalFilter quantity: DecimalFilter budgetedHours: DecimalFilter subtotal: DecimalFilter subtotalWithTax: DecimalFilter subtotalWithDiscount: DecimalFilter propertyId: IntFilter property: PropertyFilter removed: EstimateLineItemRemovedFilter AND: [EstimateLineItemFilter!] OR: [EstimateLineItemFilter!] NOT: [EstimateLineItemFilter!] eventId: IntFilter eventIsRecurring: Boolean eventItemId: IntFilter eventStatus: EstimateLineItemEventStatusFilter } input EstimateLineItemSort { id: SortOrder estimateId: SortOrder estimate: EstimateSort propertyId: SortOrder property: PropertySort date: SortOrder itemId: SortOrder item: ItemSort name: SortOrder description: SortOrder sortOrder: SortOrder price: SortOrder quantity: SortOrder budgetedHours: SortOrder subtotal: SortOrder subtotalWithTax: SortOrder subtotalWithDiscount: SortOrder } input EstimateActionFilter { equals: EstimateAction not: EstimateAction in: [EstimateAction!] notIn: [EstimateAction!] } input EstimateActionReasonFilter { equals: EstimateActionReason not: EstimateActionReason in: [EstimateActionReason!] notIn: [EstimateActionReason!] } type EstimateHistory { id: SafeInt! date: DateTime action: EstimateAction actionReason: EstimateActionReason comment: String userId: SafeInt customerId: SafeInt user: EstimateHistoryUser customer: EstimateHistoryUser } input EstimateHistoryFilter { id: IntFilter estimateId: IntFilter action: EstimateActionFilter actionReason: EstimateActionReasonFilter date: DateTimeFilter } type EstimateHistoryUser { id: SafeInt! fullName: String } input EstimateSort { id: SortOrder title: SortOrder number: SortOrder purchaseOrder: SortOrder status: SortOrder date: SortOrder subtotal: SortOrder discountPercent: SortOrder discountAmount: SortOrder tax: SortOrder total: SortOrder createdAt: SortOrder updatedAt: SortOrder requestedAt: SortOrder customer: CustomerSort acceptedAt: SortOrder } input EstimateHistorySort { id: SortOrder date: SortOrder } enum EstimateActionReason { PRICING SCHEDULING OTHER } enum EstimateAction { CREATE ACCEPT DECLINE EMAIL EMAIL_OPEN VIEW UPDATE REQUEST_CHANGES SMS INVOICE ARCHIVE MARK_EMAIL MARK_NOTSENT DROPPED_OFF UNARCHIVE MAIL PENDING MARK_MAIL LINK_COPY } """Position of an event within its recurring series""" type RecurringEventPosition { """1-based position of this event in the series""" position: Int! """ Total number of events in the series. Null for indefinitely recurring series. """ total: Int } type Event { id: SafeInt! propertyId: SafeInt property: Property location: Location """Whether this event start and end have time of day vs only dates""" hasTime: Boolean! """Date of the event""" startDate: LocalDate! """ Time of the event in the company's timezone. Null when hasTime is false. """ startTime: LocalTime """Date and time of the event in the company's timezone""" startDateTime: LocalDateTime! """ Exact inclusive last active date for a multi-day event or waitlist scheduling window; null for a single-day event. """ endDate: LocalDate """ Time the event ends in the company's timezone. Should be considered 00:00:00 if hasTime is false """ endTime: LocalTime! """Date and time that the event ends in the company's timezone""" endDateTime: LocalDateTime title: String! recurringEventId: SafeInt recurringId: SafeInt @deprecated(reason: "Use recurringEventId instead") type: EventType! """ID of the linked work request, if any.""" workRequestId: SafeInt requestId: SafeInt @deprecated(reason: "Use workRequestId instead") description: String! cost: Decimal! @deprecated """The subtotal of this event: sum of all event items before tax""" subtotal: Decimal! """ The total cost of this event: subtotal with tax. Sum of 'total' for all non-deleted line items """ total: Decimal! equipmentId: SafeInt maintenanceCost: Decimal! isInvoiceable: Boolean! invoiceId: SafeInt invoice: Invoice budgetedHours: Decimal! """Use sortOrder instead""" sortId: SafeInt! @deprecated sortOrder: SafeInt! contractId: SafeInt isNotServiced: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isClosed: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isSkipped: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isCancelled: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isWaitlisted: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") notServicedReason: String! @deprecated(reason: "Use skippedReason instead") """Date and time that the event was closed in UTC""" closedAt: DateTime """Date that the event was skipped in the company's timezone""" skippedDate: LocalDate """ Date and time that the event was skipped in UTC. Note: The CRM only sets the date portion. """ skippedAt: DateTime skippedReason: String programId: SafeInt estimateId: SafeInt categoryId: SafeInt beforeVisitCount: SafeInt! isDeleted: Boolean! createdAt: DateTime! updatedAt: DateTime estimate: Estimate lineItems(where: EventItemFilter = {isDeleted: false}, orderBy: [EventItemSort!], take: SafeInt = 1000, skip: SafeInt): [EventItem!]! """Use lineItems instead""" eventItems(where: EventItemFilter = {isDeleted: false}, orderBy: [EventItemSort!], take: SafeInt = 1000, skip: SafeInt): [EventItem!]! @deprecated(reason: "Use lineItems instead") media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! beforeAndAfters(where: BeforeAndAfterWhere, orderBy: [BeforeAndAfterSort!], take: SafeInt = 1000, skip: SafeInt): [BeforeAndAfter!]! status: EventStatus! """ Calculated status based on latest date (endDate or startDate): UPCOMING (>7 days or not set), DUE_SOON (within 7 days), OVERDUE (before today) """ waitlistDueStatus: WaitlistDueStatus tags: [String!]! customFields: [CustomField!]! """ Position of this event within its recurring series. Null if not part of a recurring series. """ recurringEventPosition: RecurringEventPosition forms: [EventForm!]! customerId: SafeInt customer: Customer createdByUserId: SafeInt! createdByUser: User lastUpdatedByUserId: SafeInt lastUpdatedByUser: User crewId: SafeInt crew: Crew color: String! completedByUserId: SafeInt completedByUser: User """ The category assigned to this event, resolved from categoryId. Null if none is set. """ category: EventCategory """Deprecated. Use users instead.""" employees: [EventEmployee!]! @deprecated(reason: "Use users") """ Users assigned to this event. Intentionally includes soft-deleted users — historical assignment records are preserved even after a user account is deactivated or deleted. To filter to active users only, pass where: { isDeleted: false }. """ users(where: UserFilter, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User!]! dispatchNotes: [DispatchNote!] timeEntries(where: TimeEntryFilter, orderBy: [TimeEntrySort!], take: SafeInt = 1000, skip: SafeInt): [TimeEntry!]! timeEntryTotals: TimeEntryTotals! recurringEvent: RecurringEvent """Route stops for this event. Ordered by stopOrder ascending.""" routeStops(where: RouteStopFilter, orderBy: [RouteStopSort!], take: SafeInt = 1000, skip: SafeInt): [RouteStop!]! """ Finds the most efficient ways to insert this event's location into existing routes, grouped by day. By default, it evaluates the event's scheduled date range (startDate to endDate). Calculates the 'marginal cost' (extra time and distance) for each possible insertion point. Returns an empty list if the event or its property lacks valid coordinates. eventFilter applies as a "some" filter for routes and a direct filter for nearbyEvents. Set fastMode: true to skip directions API calls and use crow-flies throughout. """ routeSuggestions( """Override the start date. Defaults to the event's own startDate.""" startDate: LocalDate """ Override the end date. Defaults to the event's endDate (or startDate for single-day events). """ endDate: LocalDate """ Filter by crew, customer, or other event properties. Does not affect the date range. """ eventFilter: EventFilter """ If true, use crow-flies for all calculations. No directions API API calls made. """ fastMode: Boolean ): [SchedulingDateSuggestion!]! """ The most recent upsell history for this event, if any. Used to determine if an upsell has already been sent. """ latestUpsellHistory: UpsellHistory } input EventTypeFilter { equals: EventType not: EventType in: [EventType!] notIn: [EventType!] } """ Most searches and reporting should filter to OPEN, CLOSED and/or SKIPPED """ input EventStatusFilter { equals: EventStatus not: EventStatus in: [EventStatus!] notIn: [EventStatus!] } input WaitlistDueStatusFilter { equals: WaitlistDueStatus not: WaitlistDueStatus in: [WaitlistDueStatus!] notIn: [WaitlistDueStatus!] } input EventFilter { id: IntFilter type: EventTypeFilter title: StringFilter status: EventStatusFilter waitlistDueStatus: WaitlistDueStatusFilter propertyId: IntFilter categoryId: IntFilter estimateId: IntFilter startDate: DateFilter startDateTime: DateTimeFilter endDateTime: DateTimeFilter endDate: DateFilter budgetedHours: DecimalFilter lineItems: EventItemFilter users: UserFilter isDeleted: Boolean AND: [EventFilter!] OR: [EventFilter!] NOT: [EventFilter!] customerId: IntFilter customer: CustomerFilter crewId: IntFilter """Deprecated. Use users: UserFilter instead. Map employeeUserId → id.""" employees: EventEmployeeFilter @deprecated(reason: "Use users: UserFilter instead") dispatchNotes: StringFilter @deprecated(reason: "Broken — was silently ignored. Use hasDispatchNotes: Boolean instead.") """ Filter events by whether dispatch notes exist. true = has at least one note, false = no notes (null or empty). """ hasDispatchNotes: Boolean description: StringFilter invoiceId: IntFilter recurringEventId: IntFilter recurringId: IntFilter @deprecated(reason: "Use recurringEventId instead") """ Filter events by invoiced status. Pass true to find events linked to an invoice, false to find events with no invoice. """ isInvoiced: Boolean tags: TagsFilter """ Filter events that have at least one running timer (a time entry with no endedAt). Pass true to find events with active timers, false to find events with no active timers. """ hasRunningTimers: Boolean """ Filter events that have at least one time entry. Pass true to find events with any tracked time, false to find events with no time entries. """ hasTimeEntries: Boolean """ Filter events by recurring status. Pass true to find events that belong to a recurring series, false to find one-off events. """ isRecurring: Boolean """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter createdAt: DateTimeFilter """ When true, only events that have at least one RouteStop are returned. When false, only events with no RouteStops are returned. Omit to return all events regardless of route membership. """ isRouted: Boolean } input EventWhere { id: IntFilter type: EventTypeFilter @deprecated """Use EventFilter instead""" title: StringFilter status: EventStatusFilter waitlistDueStatus: WaitlistDueStatusFilter @deprecated """Use EventFilter instead""" propertyId: IntFilter @deprecated """Use EventFilter instead""" categoryId: IntFilter @deprecated """Use EventFilter instead""" estimateId: IntFilter @deprecated """Use EventFilter instead""" startDate: DateTimeFilter @deprecated """Use EventFilter instead""" startDateTime: DateTimeFilter @deprecated """Use EventFilter instead""" endDateTime: DateTimeFilter @deprecated """Use EventFilter instead""" endDate: DateTimeFilter @deprecated """Use EventFilter instead""" isDeleted: Boolean AND: [EventFilter!] @deprecated """Use EventFilter instead""" OR: [EventFilter!] @deprecated """Use EventFilter instead""" NOT: [EventFilter!] @deprecated } input EventItemFilter { id: IntFilter name: StringFilter itemId: IntFilter item: ItemFilter isDeleted: Boolean } input EventItemSort { id: SortOrder name: SortOrder } """ Sort options for Crew. Used standalone for crew queries and nested in EventSort. """ input CrewSort { id: SortOrder name: SortOrder } input EventSort { id: SortOrder title: SortOrder startDate: SortOrder startDateTime: SortOrder startTime: SortOrder endDate: SortOrder endDateTime: SortOrder sortOrder: SortOrder status: SortOrder budgetedHours: SortOrder type: SortOrder property: PropertySort recurringEvent: RecurringEventSort customer: CustomerSort """ Sort by number of time entries (asc = least tracked first, desc = most tracked first). Note: sorting by total tracked seconds is not possible via Prisma orderBy — only _count is supported on relations. """ timeEntriesCount: SortOrder """Sort by crew name. Null-crew events sort last.""" crew: CrewSort """Sort by location name. Null-location events sort last.""" location: LocationSort updatedAt: SortOrder createdAt: SortOrder } type BeforeAndAfter { id: SafeInt! beforeDescription: String afterDescription: String isClientVisible: Boolean! eventId: SafeInt! event: Event! beforeMedia: [Media!] afterMedia: [Media!] } input BeforeAndAfterSort { id: SortOrder afterDescription: SortOrder beforeDescription: SortOrder isClientVisible: SortOrder } input BeforeAndAfterWhere { id: IntFilter afterDescription: StringFilter beforeDescription: StringFilter isClientVisible: Boolean AND: [BeforeAndAfterWhere!] OR: [BeforeAndAfterWhere!] NOT: [BeforeAndAfterWhere!] } """Event line item""" type EventItem { id: SafeInt! eventId: SafeInt! itemId: SafeInt! name: String! description: String price: Decimal! quantity: Decimal! tax: Decimal total: Decimal! budgetedHours: Decimal isDeleted: Boolean! deletedAt: DateTime createdAt: DateTime! updatedAt: DateTime event: Event! """Shouldn't be nullable but the foreign key seems to be unreliable""" item: Item } """ CLOSED='Complete' to users. OPEN and CLOSED the billable, serviceable events. Most searches and reporting should filter to OPEN, CLOSED and/or SKIPPED """ enum EventStatus { """ Not yet scheduled, so the event's startDate and endDate are just a range for potential scheduling """ WAITLISTED """Active jobs ready to be completed""" OPEN """ Skipped and not serviced. This is usually due to weather, scheduling, staffing or equipment issue """ SKIPPED """AKA 'Complete' in the UI""" CLOSED """ This isn't a used, user-facing status and should be excluded. Currently the only way to cancel an event is to cancel its recurring series, which effectively deletes it """ CANCELLED """ Deprecated. Use isDeleted: true in EventFilter instead. Soft-deleted status is not part of event status. """ DELETED @deprecated(reason: "Use isDeleted: true in EventFilter instead") } enum EventType { """Default and most commonly used type for customer jobs""" VISIT TODO MEETING MAINTENANCE WORK_REQUEST } enum WaitlistDueStatus { UPCOMING DUE_SOON OVERDUE } input EventReportOrderBy { status: SortOrder type: SortOrder customerId: SortOrder crewId: SortOrder propertyId: SortOrder recurringEventId: SortOrder completedByUserId: SortOrder categoryId: SortOrder year: SortOrder month: SortOrder startDate: SortOrder _count: EventReportCountOrderBy _sum: EventReportSumOrderBy _avg: EventReportAvgOrderBy _min: EventReportMinOrderBy _max: EventReportMaxOrderBy count: EventReportCountOrderBy @deprecated(reason: "Use _count") sum: EventReportSumOrderBy @deprecated(reason: "Use _sum") avg: EventReportAvgOrderBy @deprecated(reason: "Use _avg") min: EventReportMinOrderBy @deprecated(reason: "Use _min") max: EventReportMaxOrderBy @deprecated(reason: "Use _max") } input EventReportCountOrderBy { id: SortOrder } input EventReportSumOrderBy { budgetedHours: SortOrder } input EventReportAvgOrderBy { budgetedHours: SortOrder } input EventReportMinOrderBy { startDate: SortOrder startDateTime: SortOrder budgetedHours: SortOrder } input EventReportMaxOrderBy { startDate: SortOrder startDateTime: SortOrder budgetedHours: SortOrder } type EventReportCountAggregate { _all: SafeInt! @deprecated(reason: "Use id instead") id: SafeInt! } type EventReportSumAggregate { budgetedHours: Decimal """ Sum of 'total' for all non-deleted line items on matching events. Only available when no group-by fields are selected — throws if group-by is present. """ total: Decimal """ Sum of 'totalSeconds' for completed time entries on matching events. Excludes in-progress running timers. Only available when no group-by fields are selected — throws if group-by is present. """ timeEntrySeconds: SafeInt } type EventReportAvgAggregate { budgetedHours: Decimal } type EventReportMinAggregate { startDate: Date startDateTime: LocalDateTime budgetedHours: Decimal } type EventReportMaxAggregate { startDate: Date startDateTime: LocalDateTime budgetedHours: Decimal } input EventLineItemInput { itemId: SafeInt! price: Decimal! quantity: Decimal! description: String tax: Decimal budgetedHours: Decimal } input UpdateEventLineItemInput { itemId: SafeInt price: Decimal quantity: Decimal description: String tax: Decimal budgetedHours: Decimal } enum RecurringFrequency { DAILY WEEKLY MONTHLY YEARLY } enum RecurringDailyOption { """Repeat every N days (N = dailyDaysCount)""" EVERY_N_DAYS """Repeat every weekday (Mon–Fri)""" EVERY_WEEKDAY } enum RecurringMonthlyOption { """Repeat on a specific day of the month (e.g. the 15th)""" ON_DAY_OF_MONTH """Repeat on a weekday ordinal (e.g. the second Tuesday)""" ON_WEEKDAY_OF_MONTH } enum RecurringYearlyOption { """Repeat on a specific month and day (e.g. March 15)""" ON_MONTH_DAY """ Repeat on a weekday ordinal within a month (e.g. second Tuesday of March) """ ON_MONTH_WEEKDAY } enum RecurringOrdinal { FIRST SECOND THIRD FOURTH LAST } enum RecurringWeekday { SUN MON TUE WED THU FRI SAT } enum RecurringMonth { JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC } type RecurringEvent { id: SafeInt! companyId: SafeInt! propertyId: SafeInt property: Property title: String! type: EventType! description: String! location: String! startDate: Date! """Use startDate instead""" dateStart: Date! @deprecated(reason: "Use startDate instead") endDate: Date! startDateTime: DateTime! endDateTime: DateTime """Whether this recurring event has a specific time of day vs. all-day""" hasTime: Boolean! startTime: LocalTime! endTime: LocalTime! frequency: RecurringFrequency! dailyOption: RecurringDailyOption! dailyDaysCount: Int! weeklyCount: Int! """Monday selected""" weekday1: Boolean! """Tuesday selected""" weekday2: Boolean! """Wednesday selected""" weekday3: Boolean! """Thursday selected""" weekday4: Boolean! """Friday selected""" weekday5: Boolean! """Saturday selected""" weekday6: Boolean! """Sunday selected""" weekday7: Boolean! """Only relevant when frequency is MONTHLY""" monthlyOption: RecurringMonthlyOption monthDay: Int! monthCount1: Int! monthDayNumber: RecurringOrdinal monthWeekDay: RecurringWeekday monthCount2: Int! """Only relevant when frequency is YEARLY""" yearlyOption: RecurringYearlyOption yearMonth1: RecurringMonth yearDay: Int! yearDayNumber: RecurringOrdinal yearWeekDay: RecurringWeekday yearMonth2: RecurringMonth isInfinite: Boolean! isCancelled: Boolean! """ Timestamp when this recurring series was cancelled. Stored in the company's local timezone by the PHP layer; exposed here as UTC. Null if the series has never been cancelled (i.e. isCancelled is false). """ cancelledAt: DateTime maintenanceCost: Decimal! maintenanceTax1: Decimal! maintenanceTax1Amount: Decimal! maintenanceTax2: Decimal! maintenanceTax2Amount: Decimal! isInvoiceable: Boolean! budgetedHours: Decimal! equipmentId: SafeInt estimateId: SafeInt """ID of the linked work request, if any.""" workRequestId: SafeInt requestId: SafeInt @deprecated(reason: "Use workRequestId instead") contractId: SafeInt isChemicalApplication: Boolean! lineItems(where: RecurringEventItemFilter, orderBy: [RecurringEventItemSort!], take: SafeInt = 1000, skip: SafeInt): [RecurringEventItem!]! events(where: EventFilter, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! """Number of non-deleted events in this recurring series.""" eventCount: Int! """Use eventCount instead""" eventsCount: Int! @deprecated(reason: "Use eventCount instead") tags: [String!]! """ID of the off-season assigned to this series, if any.""" offSeasonId: SafeInt """The off-season assigned to this series, if any.""" offSeason: OffSeason """ Derived status of this recurring series. CANCELLED — isCancelled is true. PAUSED — an off-season is assigned and today falls within its date range. ACTIVE — all other cases. """ status: RecurringSeriesStatus! customerId: SafeInt customer: Customer crewId: SafeInt crew: Crew color: String! """ Users assigned to this recurring event series. Intentionally includes soft-deleted users — historical assignment records are preserved even after a user account is deactivated or deleted. To filter to active users only, pass where: { isDeleted: false }. """ users(where: UserFilter, take: SafeInt = 1000, skip: SafeInt): [User!]! } input RecurringEventItemFilter { id: IntFilter name: StringFilter } input RecurringEventItemSort { id: SortOrder name: SortOrder } type RecurringEventItem { id: SafeInt! companyId: SafeInt! recurringEventId: SafeInt! itemId: SafeInt item: Item name: String description: String price: Decimal! tax: Decimal total: Decimal quantity: Decimal! } input RecurringEventFilter { id: IntFilter companyId: IntFilter propertyId: IntFilter isCancelled: Boolean isInfinite: Boolean startDateTime: DateTimeFilter endDateTime: DateTimeFilter startDate: DateFilter """Use startDate instead""" dateStart: DateFilter @deprecated(reason: "Use startDate instead") endDate: DateFilter title: StringFilter type: EventTypeFilter estimateId: IntFilter tags: TagsFilter AND: [RecurringEventFilter!] OR: [RecurringEventFilter!] NOT: RecurringEventFilter customerId: IntFilter crewId: IntFilter } input RecurringEventSort { id: SortOrder startDateTime: SortOrder endDateTime: SortOrder startDate: SortOrder """Use startDate instead""" dateStart: SortOrder @deprecated(reason: "Use startDate instead") endDate: SortOrder title: SortOrder startTime: SortOrder frequency: SortOrder customer: CustomerSort propertyId: SortOrder customerId: SortOrder } interface FileInterface { size: SafeInt contentType: String thumb(height: SafeInt = 1200, width: SafeInt = 1200, quality: SafeInt): String url: String! } type File implements FileInterface { size: SafeInt contentType: String thumb(height: SafeInt = 1200, width: SafeInt = 1200, quality: SafeInt): String url: String! } input InvoiceReportOrderBy { status: SortOrder customerId: SortOrder propertyId: SortOrder date: SortOrder isSent: SortOrder source: SortOrder _count: InvoiceReportCountOrderBy _sum: InvoiceReportSumOrderBy _avg: InvoiceReportAvgOrderBy _min: InvoiceReportMinOrderBy _max: InvoiceReportMaxOrderBy count: InvoiceReportCountOrderBy @deprecated(reason: "Use _count") sum: InvoiceReportSumOrderBy @deprecated(reason: "Use _sum") avg: InvoiceReportAvgOrderBy @deprecated(reason: "Use _avg") min: InvoiceReportMinOrderBy @deprecated(reason: "Use _min") max: InvoiceReportMaxOrderBy @deprecated(reason: "Use _max") } input InvoiceReportCountOrderBy { id: SortOrder } input InvoiceReportSumOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } input InvoiceReportAvgOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } input InvoiceReportMinOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } input InvoiceReportMaxOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } type InvoiceReportCountAggregate { id: SafeInt! } type InvoiceReportSumAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } type InvoiceReportAvgAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } type InvoiceReportMinAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } type InvoiceReportMaxAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } enum InvoiceDeletedState { """Invoice is active""" ACTIVE """Invoice has been soft-deleted""" DELETED """Invoice was deleted as part of a cascade (e.g. parent record deleted)""" CASCADE_DELETED } enum InvoiceStatus { """Created but not yet sent to the customer""" DRAFT """Sent to the customer, awaiting payment""" PENDING """Payment received in full""" PAID """A portion has been paid, balance remains""" PARTIALLY_PAID """Payment overdue past the invoice due date""" PAST_DUE """ Written off — debt forgiven or uncollectable (shown as 'Written Off' in UI) """ WRITE_OFF } enum InvoiceHistoryAction { ARCHIVE CANCEL COMBINED CREATE DROPPED_OFF EMAIL EMAIL_OPEN LINK_COPY MAIL MARK_DRAFT MARK_EMAIL MARK_MAIL MARK_NOTSEND MARK_WRITTENOFF PAYMENT PROCESSING_FEE REFUND SMS UNARCHIVE UPDATE VIEW STRIPE_PAYMENT_FAILED } type Invoice { id: SafeInt! number: SafeInt purchaseOrder: String! date: Date! status: InvoiceStatus! title: String notes: String! terms: String! subtotal: Decimal! discountPercent: Decimal! discountAmount: Decimal! tax: Decimal! total: Decimal! paidAmount: Decimal! isSent: Boolean! hasBeenViewed: Boolean! isArchived: Boolean! contract: SafeInt deletedState: InvoiceDeletedState! lateFee: Decimal! processingFee: Decimal! propertyId: SafeInt """Timestamp of the last write to this invoice record.""" updatedAt: DateTime """@deprecated Use updatedAt instead.""" lastUpdated: DateTime @deprecated(reason: "Use updatedAt") lastAutoChargeAttempt: DateTime autoChargedAt: DateTime sentAt: DateTime sorted: Boolean! lateFeeRule: SafeInt! lateFeeRuleVersion: SafeInt! createdAt: DateTime """ The number of days this invoice is past due, considering company and customer late fee rules """ daysPastDue: Int! """The due date for this invoice, calculated based on late fee rules""" dueDate: DateTime media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! lineItems(where: InvoiceLineItemFilter, orderBy: [InvoiceLineItemSort!], take: SafeInt = 1000, skip: SafeInt): [InvoiceLineItem!]! history(where: InvoiceHistoryFilter, orderBy: [InvoiceHistorySort!], take: SafeInt = 1000, skip: SafeInt): [InvoiceHistory!]! customFields: [CustomField!]! payments(where: PaymentFilter = {isDeleted: false}, orderBy: [PaymentSort!], take: SafeInt = 1000, skip: SafeInt): [Payment!]! customerId: SafeInt! internalNotes: String sentEmail: Boolean! sentSnailMail: Boolean! droppedOffAtProperty: Boolean! tags: String! emailSubject: String! sentSms: Boolean! emailMessage: String! quickbooksLastSynced: DateTime quickbooksId: SafeInt quickbooksSyncStatus: QuickbooksSyncStatus quickbooksMeta: String quickbooksSyncFailedCount: SafeInt! quickbooksSyncFailedMessage: String isImportedFromQuickbooks: Boolean! autoSendReadyAt: DateTime autoSentAt: DateTime emailOpen: Boolean! source: String sentCopyLink: Boolean! } type InvoiceLineItem { id: SafeInt! eventId: SafeInt eventServiceId: SafeInt eventIsRecurring: Boolean! date: Date itemId: SafeInt name: String! description: String! price: Decimal! cost: Decimal! @deprecated(reason: "Use price instead") quantity: Decimal! budgetedHours: Decimal taxPercent1: Decimal! taxPercent2: Decimal! taxPrice1: Decimal! taxPrice2: Decimal! subtotal: Decimal! subtotalWithTax: Decimal! subtotalWithDiscount: Decimal! propertyId: SafeInt property: Property isRecommended: Boolean! """Rename to isRemoved?""" removed: EstimateLineItemRemoved! lineItemOrder: SafeInt! } type InvoiceHistory { id: SafeInt! date: DateTime action: InvoiceHistoryAction actionReason: String comment: String userId: SafeInt customerId: SafeInt user: InvoiceHistoryUser customer: InvoiceHistoryUser meta: String payment: Payment invoice: Invoice } type InvoiceHistoryUser { id: SafeInt! fullName: String } input InvoiceLineItemFilter { id: IntFilter name: StringFilter } input InvoiceLineItemSort { id: SortOrder name: SortOrder lineItemOrder: SortOrder } input InvoiceHistoryFilter { id: IntFilter action: StringFilter } input InvoiceHistorySort { id: SortOrder date: SortOrder } input InvoiceStatusFilter { equals: InvoiceStatus not: InvoiceStatus in: [InvoiceStatus!] notIn: [InvoiceStatus!] } input InvoiceDeletedStateFilter { equals: InvoiceDeletedState not: InvoiceDeletedState in: [InvoiceDeletedState!] notIn: [InvoiceDeletedState!] } input InvoiceFilter { id: IntFilter number: IntFilter purchaseOrder: StringFilter date: DateFilter status: InvoiceStatusFilter title: StringFilter isArchived: Boolean deletedState: InvoiceDeletedStateFilter """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter createdAt: DateTimeFilter customerId: IntFilter } input InvoiceSort { id: SortOrder number: SortOrder purchaseOrder: SortOrder companyId: SortOrder customerId: SortOrder date: SortOrder status: SortOrder title: SortOrder sent: SortOrder updatedAt: SortOrder createdAt: SortOrder } enum ItemMarkupType { """No markup — price is the flat item price""" NONE """Percentage markup: item_markup is a percentage added on top of cost""" PERCENTAGE """ Flat dollar markup: item_markup is a fixed dollar amount added on top of cost """ FLAT } enum ItemType { SERVICE ITEM LABOR OTHER } type ItemCategory { id: SafeInt! name: String! description: String! isRoot: Boolean! items(where: ItemFilter = {isDeleted: false}, orderBy: [ItemSort!], take: SafeInt = 1000, skip: SafeInt): [Item!]! } input ItemCategoryFilter { id: IntFilter } input ItemCategorySort { id: SortOrder name: SortOrder } input ItemFilter { category: ItemCategoryFilter events: EventFilter id: IntFilter isDeleted: Boolean name: StringFilter description: StringFilter price: DecimalFilter rateChargedP4p: DecimalFilter inventoryOnHand: IntFilter wholesaleCost: DecimalFilter markup: DecimalFilter vendor: VendorFilter AND: [ItemFilter!] OR: [ItemFilter!] NOT: ItemFilter } type Item { id: SafeInt! categoryId: SafeInt! category: ItemCategory type: ItemType! name: String! description: String! price: Decimal! quantity: Decimal! budgetedHours: Decimal tax1: Decimal! tax2: Decimal! unit: String! supplier: String! supplierId: SafeInt! materialCost: Decimal! equipmentCost: Decimal! subcontractCost: Decimal! isDeleted: Boolean! hasInventory: Boolean! isGlobal: Boolean! isTaxable: Boolean! color: String vendor: Vendor eventCount(where: EventFilter): SafeInt! wholesaleCost: Decimal! inventoryOnHand: SafeInt! markup: Decimal! markupType: ItemMarkupType! rateChargedP4p: Decimal! } input ItemSort { id: SortOrder name: SortOrder categoryId: SortOrder category: ItemCategorySort } type ItemPackage { id: Int! name: String description: String! categoryId: Int! itemIds: [SafeInt!]! items: [Item!]! isDeleted: Boolean! createdAt: DateTime! } input ItemPackageFilter { id: IntFilter name: StringFilter categoryId: IntFilter isDeleted: Boolean AND: [ItemPackageFilter!] OR: [ItemPackageFilter!] NOT: ItemPackageFilter } input ItemPackageSort { id: SortOrder name: SortOrder createdAt: SortOrder } type Location { name: String latitude: Decimal! longitude: Decimal! } input LocationInput { latitude: Decimal! longitude: Decimal! } input LocationSort { name: SortOrder } enum MediaModule { TEMPORARY BEFORE_AFTER CUSTOMER INVOICE VISIT ASSET EQUIPMENT ESTIMATE EXPENSE RECURRING VENDOR CHEMICAL CHEMICAL_APP } type Media implements FileInterface { id: SafeInt! companyId: SafeInt! module: MediaModule! objectId: SafeInt! fileName: String! size: SafeInt contentType: String thumb(height: SafeInt = 1200, width: SafeInt = 1200, quality: SafeInt): String url: String! description: String! uploadedAt: DateTime movedToFolderAt: DateTime } input MediaSort { fileName: SortOrder size: SortOrder contentType: SortOrder uploadedAt: SortOrder } input MediaWhere { fileName: StringFilter size: IntFilter description: StringFilter uploadedAt: DateTimeFilter } enum OrderGateway { AUTHORIZE STRIPE } enum OrderPaymentType { EMPTY PAYPAL VISA MASTERCARD AMEX DISCOVER UNKNOWN } type Order { id: SafeInt! date: DateTime! companyId: SafeInt userId: SafeInt! status: String! productId: SafeInt! productSku: String! """ Plan/package derived from productSku when it matches a known Stripe price """ plan: Plan productName: String! quantity: SafeInt! amount: Decimal! paymentType: OrderPaymentType! transactionId: String! last4: String! isRecurring: Boolean! isStoreOrder: Boolean! gateway: OrderGateway! stripeInvoiceId: String! requiresShipping: Boolean! receiptUrl: String } input OrderFilter { id: IntFilter companyId: IntFilter date: DateTimeFilter status: StringFilter productSku: StringFilter productName: StringFilter amount: DecimalFilter transactionId: StringFilter requiresShipping: Boolean AND: [OrderFilter!] OR: [OrderFilter!] NOT: [OrderFilter!] } input OrderSort { id: SortOrder date: SortOrder status: SortOrder amount: SortOrder companyId: SortOrder productSku: SortOrder productName: SortOrder } input PaymentReportOrderBy { date: SortOrder method: SortOrder customerId: SortOrder isRefund: SortOrder _count: PaymentReportCountOrderBy _sum: PaymentReportSumOrderBy _avg: PaymentReportAvgOrderBy _min: PaymentReportMinOrderBy _max: PaymentReportMaxOrderBy count: PaymentReportCountOrderBy @deprecated(reason: "Use _count") sum: PaymentReportSumOrderBy @deprecated(reason: "Use _sum") avg: PaymentReportAvgOrderBy @deprecated(reason: "Use _avg") min: PaymentReportMinOrderBy @deprecated(reason: "Use _min") max: PaymentReportMaxOrderBy @deprecated(reason: "Use _max") } input PaymentReportCountOrderBy { id: SortOrder } input PaymentReportSumOrderBy { totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } input PaymentReportAvgOrderBy { totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } input PaymentReportMinOrderBy { date: SortOrder totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } input PaymentReportMaxOrderBy { date: SortOrder totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } type PaymentReportCountAggregate { id: SafeInt! } type PaymentReportSumAggregate { totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } type PaymentReportAvgAggregate { totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } type PaymentReportMinAggregate { date: Date totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } type PaymentReportMaxAggregate { date: Date totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } enum PaymentMethod { CASH CHECK CREDIT_CARD CUSTOMER_CREDIT_BALANCE ACH_TRANSFER PAYPAL OTHER DEBIT_CARD DIRECT_DEBIT BACS VENMO WISETACK ZELLE CASH_APP APPLE_PAY EFT } type Payment { id: SafeInt! customerId: SafeInt customer: Customer method: PaymentMethod methodDisplayName: String date: Date! """Stores the invoice/payment amount without processing fee""" totalAmount: Decimal! invoiceAmount: Decimal! invoiceId: SafeInt creditAmount: Decimal! notes: String! details: String isRefund: Boolean! refundedAt: DateTime tipAmount: Decimal! processFeeCharged: Decimal! processFeeChargedTo: PaymentProcessFeePayer """Represents the actual amount the customer paid (including fee)""" paidAmount: Decimal! createdAt: DateTime updatedAt: DateTime invoice: Invoice userId: SafeInt expenseId: SafeInt """Resolved payee for expense payments (null for non-expense payments).""" payee: PaymentPayee } enum PaymentProcessFeePayer { CUSTOMER } input PaymentSort { id: SortOrder method: SortOrder date: SortOrder totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder refundedAt: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder createdAt: SortOrder updatedAt: SortOrder } input PaymentMethodFilter { equals: PaymentMethod not: PaymentMethod in: [PaymentMethod!] notIn: [PaymentMethod!] } input PaymentFilter { id: IntFilter method: PaymentMethodFilter date: DateFilter totalAmount: DecimalFilter invoiceAmount: DecimalFilter creditAmount: DecimalFilter notes: StringFilter details: StringFilter isRefund: Boolean isDeleted: Boolean refundedAt: DateTimeFilter tipAmount: DecimalFilter processFeeCharged: DecimalFilter customerId: IntFilter invoiceId: IntFilter expenseId: IntFilter """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter createdAt: DateTimeFilter isExpensePayment: Boolean hasTip: Boolean } enum SubscriptionTier { """Legacy feature set""" ENTERPRISE_LEGACY """Legacy feature set""" PRO STARTER SOLO GROWTH ENTERPRISE } enum BillingInterval { MONTHLY YEARLY } type Plan { sku: String! name: String! description: String! price: Decimal! tier: Tier! billingInterval: BillingInterval! """Is part of current pricing. If false, it's a legacy plan""" isCurrent: Boolean! isFreeTrial: Boolean! isAddon: Boolean! } type Tier { id: SubscriptionTier! level: Int! name: String! features: TierFeatures! """Price per additional SMS segment beyond the included allowance""" smsPrice: Decimal! """Price per additional user seat beyond the included allowance""" additionalUserSeatPrice: Decimal! } type TierFeatures { generalContactLimit: Int! hasCommunicationAccess: Boolean! hasApplicationFee: Boolean! hasCustomerPortal: Boolean! hasSimpleAutomations: Boolean! hasUpsells: Boolean! hasEmailTemplates: Boolean! hasEmailLinks: Boolean! hasIntercom: Boolean! hasTipping: Boolean! hasBackups: Boolean! hasProTimer: Boolean! hasRouteOptimization: Boolean! hasChemicalApplication: Boolean! hasPaymentMethodRequired: Boolean! hasSMS: Boolean! hasQuickbooksSync: Boolean! hasPriceMatrix: Boolean! hasAutoCharge: Boolean! hasACHPayments: Boolean! hasAdvancedAutomations: Boolean! hasPassThroughFees: Boolean! hasPayInFull: Boolean! hasWorkRequests: Boolean! hasEstimateDeposits: Boolean! hasCallNotes: Boolean! hasWaitlist: Boolean! includedUsers: Int! includedSmsSegments: Int! hasApiAccess: Boolean! hasApiTrial: Boolean! workRequests: Boolean @deprecated payInFull: Boolean @deprecated tipping: Boolean @deprecated } """Deletion/archive status of a property.""" enum PropertyDeletedStatus { """Property is active and in use.""" ACTIVE """Property has been explicitly deleted.""" DELETED """ Property is hidden because its parent customer was deactivated. Restored automatically when the customer is reactivated. """ CUSTOMER_DEACTIVATED } """ Unit of measurement for property area fields (size, pavedSize, houseSize, etc.). """ enum AreaMeasurement { """Square feet (default)""" SQ_FEET """Square meters""" SQ_METERS """Acres""" ACRES """Hectares""" HECTARES } type PropertyCrew { id: SafeInt! crewId: SafeInt! companyId: SafeInt! assignedAt: DateTime status: SafeInt! notes: String } type Property { id: SafeInt! requestId: SafeInt name: String! address: Address """ The property's address, geocoded as a coordinate pair. Null if not yet geocoded. """ location: Location size: Decimal! sizeType: AreaMeasurement! pavedSize: Decimal! pavedSizeType: AreaMeasurement! houseSize: Decimal! houseSizeType: AreaMeasurement! plantingSize: Decimal! plantingSizeType: AreaMeasurement! roofAreaSize: Decimal! roofAreaSizeType: AreaMeasurement! roofPerimeterSize: Decimal! roofPerimeterSizeType: AreaMeasurement! drivewayWalkwaySize: Decimal! drivewayWalkwaySizeType: AreaMeasurement! roofPitch: SafeInt fence: String storiesTall: SafeInt notes: String! """ Deletion/archive status. Use ACTIVE for normal properties. DELETED means explicitly removed; CUSTOMER_DEACTIVATED means the parent customer was deactivated (auto-restored on reactivation). """ deletedStatus: PropertyDeletedStatus! isActive: Boolean! tags: String! tax1: Decimal! tax2: Decimal! tax3: Decimal! taxTotal: Decimal! createdAt: DateTime updatedAt: DateTime media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! customer: Customer customerId: SafeInt! lastServiceDate: DateTime propertyCrews: [PropertyCrew!]! } input PropertyFilter { id: IntFilter address: AddressFilter name: StringFilter deletedStatus: PropertyDeletedStatus isActive: Boolean customerId: IntFilter customer: CustomerFilter size: DecimalFilter } """ Input for creating a property. `customerId` and `name` are required; all other fields are optional. """ input PropertyInput { """The customer this property belongs to.""" customerId: SafeInt! """Display name for the property (e.g. "123 Main St Property").""" name: String! address: AddressInput notes: String """Unit of measurement for all size fields. Defaults to SQ_FEET.""" sizeType: AreaMeasurement """ Tag names to attach. Tags are created if they don't already exist for this company. """ tags: [String!] """ Custom field values to set on the property at creation time. Each fieldId must reference a DynamicCustomField with objectType = ASSET for this company. """ customFields: [CustomFieldInput!] """Crews to assign. Each must belong to this company.""" crewIds: [SafeInt!] """ S3 temp file keys to attach as media. Obtain keys via the createUpload mutation: call createUpload, PUT the file to the returned uploadUrl, then pass the key here. """ mediaFileKeys: [String!] } """ Input for updating a property. All fields are optional — only provided fields are written. """ input UpdatePropertyInput { """Display name for the property.""" name: String notes: String address: AddressInput """Unit of measurement for all size fields.""" sizeType: AreaMeasurement size: Decimal pavedSize: Decimal houseSize: Decimal plantingSize: Decimal roofAreaSize: Decimal roofPerimeterSize: Decimal drivewayWalkwaySize: Decimal """ Crew IDs to assign. Replace-set semantics — supplying this field replaces the current crew set; omitting it leaves crews untouched. """ crewIds: [SafeInt!] """ Tag names to attach. Replace-set semantics — supplying this field replaces the current tag set; omitting it leaves tags untouched. """ tags: [String!] """ Custom field values to set. Replace-set semantics — supplying this field upserts by fieldId; omitting it leaves custom fields untouched. """ customFields: [CustomFieldInput!] } input PropertySort { id: SortOrder name: SortOrder address: AddressSort createdAt: SortOrder updatedAt: SortOrder } type Rating { id: SafeInt! value: Float! note: String! createdAt: DateTime! serviceName: String! } input SmsReportCountOrderBy { id: SortOrder } input SmsReportSumOrderBy { segments: SortOrder } input SmsReportAvgOrderBy { segments: SortOrder } input SmsReportMinOrderBy { segments: SortOrder createdAt: SortOrder sentAt: SortOrder } input SmsReportMaxOrderBy { segments: SortOrder createdAt: SortOrder sentAt: SortOrder } input SmsReportOrderBy { direction: SortOrder status: SortOrder stripeStatus: SortOrder customerId: SortOrder counterPartyUserId: SortOrder sentByUserId: SortOrder conversationId: SortOrder isViewed: SortOrder isDone: SortOrder _count: SmsReportCountOrderBy _sum: SmsReportSumOrderBy _avg: SmsReportAvgOrderBy _min: SmsReportMinOrderBy _max: SmsReportMaxOrderBy count: SmsReportCountOrderBy @deprecated(reason: "Use _count") sum: SmsReportSumOrderBy @deprecated(reason: "Use _sum") avg: SmsReportAvgOrderBy @deprecated(reason: "Use _avg") min: SmsReportMinOrderBy @deprecated(reason: "Use _min") max: SmsReportMaxOrderBy @deprecated(reason: "Use _max") } type SmsReportCountAggregate { id: SafeInt! } type SmsReportSumAggregate { segments: SafeInt } type SmsReportAvgAggregate { segments: Float } type SmsReportMinAggregate { segments: SafeInt createdAt: DateTime sentAt: DateTime } type SmsReportMaxAggregate { segments: SafeInt createdAt: DateTime sentAt: DateTime } type SmsReportRow { direction: SmsDirection status: SmsStatus stripeStatus: SmsStripeStatus customerId: SafeInt """ The user/employee on the other end of this conversation (sms_employee_id) """ counterPartyUserId: SafeInt """The Homeworks user who sent the outbound message (sms_user_id)""" sentByUserId: SafeInt conversationId: String isViewed: Boolean isDone: Boolean customer: Customer counterPartyUser: User sentByUser: User _count: SmsReportCountAggregate _sum: SmsReportSumAggregate _avg: SmsReportAvgAggregate _min: SmsReportMinAggregate _max: SmsReportMaxAggregate count: SmsReportCountAggregate @deprecated(reason: "Use _count") sum: SmsReportSumAggregate @deprecated(reason: "Use _sum") avg: SmsReportAvgAggregate @deprecated(reason: "Use _avg") min: SmsReportMinAggregate @deprecated(reason: "Use _min") max: SmsReportMaxAggregate @deprecated(reason: "Use _max") } enum SmsStatus { SENT DELIVERED FAILED QUEUED SENDING UNDELIVERED RECEIVING RECEIVED ACCEPTED SCHEDULED READ PARTIALLY_DELIVERED CANCELED } enum SmsDirection { IN OUT } enum SmsStripeStatus { EXEMPT PENDING POSTED } input SmsStatusFilter { equals: SmsStatus not: SmsStatus in: [SmsStatus!] notIn: [SmsStatus!] } input SmsDirectionFilter { equals: SmsDirection not: SmsDirection in: [SmsDirection!] notIn: [SmsDirection!] } input SmsStripeStatusFilter { equals: SmsStripeStatus not: SmsStripeStatus in: [SmsStripeStatus!] notIn: [SmsStripeStatus!] } enum StripeWalletType { apple_pay google_pay cash_app_pay samsung_pay link masterpass visa_checkout amex_express_checkout } enum StripePaymentMethodType { billie card crypto acss_debit affirm afterpay_clearpay alipay alma amazon_pay au_becs_debit bacs_debit bancontact blik boleto card_present cashapp customer_balance eps fpx giropay grabpay ideal interac_present kakao_pay klarna konbini kr_card link mobilepay multibanco naver_pay nz_bank_account oxxo p24 pay_by_bank payco paynow paypal pix promptpay revolut_pay samsung_pay satispay sepa_debit sofort swish twint us_bank_account wechat_pay zip } enum StripePaymentMethodAllowRedisplay { always limited unspecified } type StripePaymentMethod { id: String! object: String! type: StripePaymentMethodType! created: DateTime! allow_redisplay: StripePaymentMethodAllowRedisplay livemode: Boolean! metadata: JSON billing_details: JSON! status: StripePaymentMethodStatus verificationMethod: StripeVerificationMethod card: JSON acss_debit: JSON affirm: JSON afterpay_clearpay: JSON alipay: JSON alma: JSON amazon_pay: JSON au_becs_debit: JSON bacs_debit: JSON bancontact: JSON blik: JSON boleto: JSON card_present: JSON cashapp: JSON customer_balance: JSON eps: JSON fpx: JSON giropay: JSON grabpay: JSON ideal: JSON interac_present: JSON kakao_pay: JSON klarna: JSON konbini: JSON kr_card: JSON link: JSON mobilepay: JSON multibanco: JSON naver_pay: JSON oxxo: JSON p24: JSON pay_by_bank: JSON payco: JSON paynow: JSON paypal: JSON pix: JSON promptpay: JSON revolut_pay: JSON samsung_pay: JSON sepa_debit: JSON sofort: JSON swish: JSON twint: JSON us_bank_account: JSON wechat_pay: JSON zip: JSON } enum StripePaymentMethodStatus { VERIFIED UNVERIFIED PENDING_VERIFICATION } enum StripeVerificationMethod { amounts descriptor_code } enum StripePaymentSourceType { bank_account card } union StripePaymentSource = StripeCard | StripeBankAccount """A card object from Stripe.""" type StripeCard { """Unique identifier for the object.""" id: String! """String representing the object's type. Should always be "card".""" object: StripePaymentSourceType! """Indicates if this card can be redisplayed in a checkout flow.""" allow_redisplay: String """Available payout methods for this card.""" available_payout_methods: [String] """Card brand.""" brand: String! """Two-letter ISO code representing the card's country.""" country: String """Currency supported by the card (if applicable).""" currency: Currency """The customer this card belongs to.""" customer: String """Result of the CVC check: pass, fail, unavailable, or unchecked.""" cvc_check: String """Whether this is the default external account for its currency.""" default_for_currency: Boolean """Always true for a deleted object.""" deleted: Boolean """Internal description of card type.""" description: String """Last 4 digits of device account number (tokenized cards).""" dynamic_last4: String """Expiration month.""" exp_month: Int! """Expiration year.""" exp_year: Int! """Card fingerprint for identifying duplicate cards.""" fingerprint: String """Funding type: credit, debit, prepaid, or unknown.""" funding: String! """Issuer identification number.""" iin: String """Name of the issuing bank.""" issuer: String """Last 4 digits of the card.""" last4: String! """Key-value metadata attached to the card.""" metadata: JSON """Cardholder name.""" name: String """Status based on the card issuer.""" regulated_status: String """Status of the card: new, errored, etc.""" status: String """Method used to tokenize the card.""" tokenization_method: String } """A bank account object from Stripe.""" type StripeBankAccount { """Unique identifier for the object.""" id: String! """ String representing the object's type. Should always be "bank_account". """ object: StripePaymentSourceType! """The name of the person or business that owns the bank account.""" account_holder_name: String """ The type of entity that holds the account. This can be either `individual` or `company`. """ account_holder_type: StripeBankAccountHolderType """ The bank account type. This can only be `checking` or `savings` in most countries. In Japan, this can only be `futsu` or `toza`. """ account_type: String """A set of available payout methods for this bank account.""" available_payout_methods: [String] """ Name of the bank associated with the routing number (e.g., `WELLS FARGO`). """ bank_name: String """ Two-letter ISO code representing the country the bank account is located in. """ country: String """Three-letter ISO code for the currency paid out to the bank account.""" currency: Currency """The ID of the customer that the bank account is associated with.""" customer: String """ Whether this bank account is the default external account for its currency. """ default_for_currency: Boolean """Always true for a deleted object""" deleted: Boolean """Uniquely identifies this particular bank account.""" fingerprint: String """The last four digits of the bank account number.""" last4: String! """Key-value metadata that can be attached to the object.""" metadata: JSON """The routing transit number for the bank account.""" routing_number: String """The verification status of the bank account.""" status: StripeBankAccountStatus! } enum StripeBankAccountStatus { new validated verified verification_failed errored } enum StripeBankAccountHolderType { individual company } type StripeSourceObject { id: ID! object: String! type: String usage: String status: String } enum StripeIntentType { PAYMENT SETUP } enum StripePaymentIntentStatus { canceled processing requires_action requires_capture requires_confirmation requires_payment_method succeeded } type StripePaymentIntent { stripeIntentId: String! clientSecret: String! stripeConnectAccountId: String! customerSessionClientSecret: String subtotal: SafeInt! tip: SafeInt! stripeProcessingFee: SafeInt! customerProcessingFee: SafeInt! applicationFee: SafeInt! amount: SafeInt! currency: Currency! paymentMethodType: StripePaymentMethodType walletType: StripeWalletType } type StripeSetupIntent { stripeIntentId: String! clientSecret: String! stripeConnectAccountId: String! customerSessionClientSecret: String! } type StripeIntent { stripeIntentId: String! stripeCustomerId: String! clientSecret: String @deprecated customerSessionClientSecret: String @deprecated type: StripeIntentType! customerId: SafeInt! invoiceId: SafeInt estimateId: SafeInt status: StripePaymentIntentStatus! errorType: String errorCode: String errorMessage: String createdAt: DateTime! updatedAt: DateTime! } """Company connected stripe information""" type Stripe { id: SafeInt! publishableKey: String stripeAccountId: String } enum Currency { USD CAD EUR GBP AUD CHF CZK DKK HKD HUF JPY NOK NZD PLN SEK SGD RUB UAH ZAR CNY INR SRD } """ Mirrors Stripe subscription statuses plus CANCEL_REQUESTED for scheduled cancellations """ enum SubscriptionStatus { CURRENT PAST_DUE UNPAID CANCELLED CANCEL_REQUESTED INCOMPLETE INCOMPLETE_EXPIRED TRIALING PAUSED } """Why a customer is canceling their subscription.""" enum CancellationReason { TOO_EXPENSIVE HARD_TO_USE MISSING_FEATURES TECHNICAL_ISSUES DIFFICULT_ONBOARDING OTHER } """What the customer valued most, even though they're leaving.""" enum SatisfactionHighlight { EASE_OF_USE CUSTOMER_SUPPORT GREAT_COMMUNITY VALUE_FOR_PRICE GREAT_ONBOARDING OTHER } """Last payment failure details (resolved on demand from Stripe)""" type PaymentError { """Stripe decline code (e.g., 'card_declined', 'insufficient_funds')""" code: String """Stripe decline reason code (e.g., 'generic_decline')""" declineCode: String """Human-readable error message""" message: String } type CompanySubscription { """Current plan SKU""" planPriceId: String! """Scheduled downgrade plan SKU""" pendingPriceId: String """Subscription status""" status: SubscriptionStatus! """Stripe subscription ID""" stripeSubscriptionId: String """Amount past due in cents""" pastDueAmountCents: SafeInt! """Grace period end date for failed payments""" gracePeriodEndsOn: DateTime """Current billing period end date""" planPeriodEndsOn: DateTime """Next charge date""" nextChargeOn: DateTime """Default payment method expiration month""" cardExpMonth: Int """Default payment method expiration year""" cardExpYear: Int """Plan details derived from planPriceId""" plan: Plan! """ Pending plan details derived from pendingPriceId (if scheduled downgrade exists) """ pendingPlan: Plan """Tier derived from planPriceId""" tier: Tier! """ Pending tier derived from pendingPriceId (if scheduled downgrade exists) """ pendingTier: Tier """Subscription created date""" createdAt: DateTime! """Subscription last updated date""" updatedAt: DateTime! """ Last payment error (resolved on demand from Stripe; null when status is CURRENT/TRIALING) """ lastPaymentError: PaymentError """ Amount (cents) of the next upcoming invoice from Stripe, reflecting any active discounts or prorations. Null for free tier, cancelled subscriptions, or when Stripe is unreachable. Prefer this over plan.price for billing UI. """ nextInvoiceAmountCents: SafeInt """ Start of the current monthly billing period (sourced from Stripe). Null until the company is on flexible billing. """ monthlyPeriodStartedAt: DateTime """ End of the current monthly billing period (sourced from Stripe). Null until the company is on flexible billing. """ monthlyPeriodEndsAt: DateTime """ Approximate usage and charges for the current billing period. Null if the period has not yet been populated. """ monthlyUsage: SubscriptionMonthlyUsage } """ Approximate usage and cost picture for the current monthly billing period. """ type SubscriptionMonthlyUsage { """ Number of outbound SMS messages sent this billing period (PENDING + POSTED only) """ smsMessagesSent: SafeInt! """ Number of SMS segments accumulated this billing period (PENDING + POSTED only) """ smsSegmentsSent: SafeInt! """ Approximate SMS charge for this period based on segments beyond the included allowance """ approximateSmsCost: Decimal! """Number of currently active user seats""" activeUserSeats: SafeInt! """ Additional seats beyond the tier's included allowance (max(activeUserSeats - tier.includedUserSeats, 0)) """ additionalUserSeats: SafeInt! """Approximate charge for additional user seats this billing cycle""" approximateUserSeatCost: Decimal! } type TempFile { key: String! contentType: String! uploadUrl: String! downloadUrl: String expires: SafeInt! } input TimeEntryReportOrderBy { userId: SortOrder categoryId: SortOrder eventId: SortOrder customerId: SortOrder propertyId: SortOrder _count: TimeEntryReportCountOrderBy _sum: TimeEntryReportSumOrderBy _avg: TimeEntryReportAvgOrderBy _min: TimeEntryReportMinOrderBy _max: TimeEntryReportMaxOrderBy count: TimeEntryReportCountOrderBy @deprecated(reason: "Use _count") sum: TimeEntryReportSumOrderBy @deprecated(reason: "Use _sum") avg: TimeEntryReportAvgOrderBy @deprecated(reason: "Use _avg") min: TimeEntryReportMinOrderBy @deprecated(reason: "Use _min") max: TimeEntryReportMaxOrderBy @deprecated(reason: "Use _max") } input TimeEntryReportCountOrderBy { id: SortOrder } input TimeEntryReportSumOrderBy { totalSeconds: SortOrder paymentAmount: SortOrder bonusPaymentAmount: SortOrder } input TimeEntryReportAvgOrderBy { totalSeconds: SortOrder hourlyRate: SortOrder paymentAmount: SortOrder bonusPaymentAmount: SortOrder } input TimeEntryReportMinOrderBy { totalSeconds: SortOrder hourlyRate: SortOrder paymentAmount: SortOrder startedAt: SortOrder endedAt: SortOrder } input TimeEntryReportMaxOrderBy { totalSeconds: SortOrder hourlyRate: SortOrder paymentAmount: SortOrder startedAt: SortOrder endedAt: SortOrder } type TimeEntryReportCountAggregate { id: SafeInt! } type TimeEntryReportSumAggregate { totalSeconds: SafeInt paymentAmount: Decimal bonusPaymentAmount: Decimal } type TimeEntryReportAvgAggregate { totalSeconds: Float hourlyRate: Decimal paymentAmount: Decimal bonusPaymentAmount: Decimal } type TimeEntryReportMinAggregate { totalSeconds: SafeInt hourlyRate: Decimal paymentAmount: Decimal startedAt: LocalDateTime endedAt: LocalDateTime } type TimeEntryReportMaxAggregate { totalSeconds: SafeInt hourlyRate: Decimal paymentAmount: Decimal startedAt: LocalDateTime endedAt: LocalDateTime } enum UserStatus { BLOCKED ACTIVE SUSPENDED ADDON_ACTIVE } enum UserRole { OWNER ADMIN EMPLOYEE } input UserRoleFilter { equals: UserRole not: UserRole in: [UserRole!] notIn: [UserRole!] } input UserFilter { id: IntFilter email: StringFilter firstName: StringFilter lastName: StringFilter status: UserStatus isDeleted: Boolean lastLogin: DateTimeFilter role: UserRoleFilter events: EventFilter excludeFromCapacity: Boolean """ Searches across email, contactEmail, phone, cell, firstName, lastName (case-insensitive contains), and id (exact match) """ search: String AND: [UserFilter!] OR: [UserFilter!] NOT: UserFilter } input UserSort { id: SortOrder email: SortOrder firstName: SortOrder lastName: SortOrder role: SortOrder status: SortOrder isDeleted: SortOrder lastLogin: SortOrder } """ Shared User type used by both Admin and Company APIs. Company extends this with estimateHistory, events, crews, permissions. """ type User { id: SafeInt! email: String! firstName: String! lastName: String! role: UserRole! status: UserStatus! address: Address! phone: String! fax: String! cell: String! contactEmail: String! notes: String title: String! picture: File! lastLogin: DateTime! votes: SafeInt! isDeleted: Boolean! license: String tags: String! partner: SafeInt! latitude: String @deprecated(reason: "Use lastKnownLocation instead") longitude: String @deprecated(reason: "Use lastKnownLocation instead") """ The user's last known GPS location (e.g. from the mobile app). Not based on their home/mailing address. Null if never reported. """ lastKnownLocation: Location """ The employee's on-file address resolved to map coordinates. Suitable as a route start/end point. Null if address is blank or geocoding fails. Use this — not lastKnownLocation — for route endpoint selection. lastKnownLocation is a GPS ping, not where the employee starts the day. """ addressLocation: Location locationStoredAt: DateTime locationLastFetchAttemptedAt: DateTime inactiveDate: DateTime dashboardLimit: SafeInt! """ Per-user day-view color map (JSON array). Controls event colors specifically in the day view of the scheduler. """ dayColor: String! """ Default fallback color for the scheduler when no per-category color is set (JSON array). """ scheduleDefaultColor: String! """ User's display color (hex string). Used for the user chip/avatar in the scheduler. """ color: String allowUpsells: Boolean! company: Company! estimateHistory(where: EstimateHistoryFilter, orderBy: [EstimateHistorySort!], take: SafeInt = 1000, skip: SafeInt): [EstimateHistory] permissions: Permissions! """Per-date available hours overrides for this user.""" dayOverrides(where: UserDayOverrideFilter, orderBy: [UserDayOverrideSort!], take: SafeInt = 1000, skip: SafeInt): [UserDayOverride!]! hireDate: Date! birthDate: Date! rate: Decimal! """ Pay rate period/unit. TODO: convert to enum UserRateType (Hour | Day | Week | Month | Year). Check P4P and Field Force for breaking GraphQL query changes before migrating. """ rateType: String! """ Pay performance type. TODO: convert to enum UserPayPerfType. Values: 1=Rate, 2=Rate+flat per job, 3=Rate+percent per job, 4=Flat rate, 5=Flat+percent per job, 6=Percent per job. Check P4P and Field Force for breaking GraphQL query changes before migrating. """ payPerfType: SafeInt! """ Pay performance amount — numeric value that works with payPerfType (e.g. the percentage or flat amount). TODO: rename once payPerfType is an enum. """ payPerf: String! """ Default daily available hours used for capacity planning and scheduling (e.g. 8.0 = a standard full day). """ dailyCapacityHours: Decimal! """ Maximum billable hours per week for this user. Used as a cap when rolling up company-wide capacity. """ weeklyCapacityHours: SafeInt! """ When true, this user is excluded from company-wide capacity calculations (e.g. office staff, non-field employees). """ excludeFromCapacity: Boolean! clockInRequired: Boolean! """ Per-event-type color overrides for the schedule view. Unset keys fall back to the default (#3a87ab). """ scheduleColors: ScheduleColors """ Raw JSON string for schedule colors. Use scheduleColors for typed access. """ scheduleColor: String @deprecated(reason: "Use scheduleColors") """ Number of non-deleted events assigned to this user. Pass where to narrow the count. """ eventCount(where: EventFilter): SafeInt! """ Events assigned to this user. Defaults to non-deleted events. Pass where: { isDeleted: true } to include deleted events. """ events(where: EventFilter = {isDeleted: false}, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event] """Recurring event series this user is assigned to.""" recurringEvents(where: RecurringEventFilter, orderBy: [RecurringEventSort!], take: SafeInt = 1000, skip: SafeInt): [RecurringEvent] """Crews this user belongs to. Defaults to non-deleted crews.""" crews(where: CrewFilter = {isDeleted: false}, orderBy: [CrewSort!], take: SafeInt = 1000, skip: SafeInt): [Crew] } type Permissions { calendar: [PermissionCalendar!]! customers: [Permission!]! estimates: [Permission!]! expenses: [Permission!]! invoices: [Permission!]! payments: [Permission!]! settings: [Permission!]! timeTracking: [PermissionTimeTracking!]! automations: [Permission!]! backup: [Permission!]! billingSettings: [Permission!]! calculator: [Permission!]! chemicalApplications: [Permission!]! chemicalTracking: [Permission!]! cloudStorage: [Permission!]! contracts: [Permission!]! crews: [Permission!]! customFields: [Permission!]! dispatchBoard: [Permission!]! documents: [Permission!]! emails: [Permission!]! users: [Permission!]! equipment: [Permission!]! items: [Permission!]! kpiCockpit: [Permission!]! marketplace: [Permission!]! notes: [Permission!]! properties: [Permission!]! reports: [Permission!]! texts: [Permission!]! visitForms: [Permission!]! vendors: [Permission!]! franchises: [Permission!]! @deprecated(reason: "Not referenced in copilot-api or copilot PHP; not in pages table seed — likely legacy") canSkipEvents: Boolean! @deprecated(reason: "Use Permissions.calendar.canSkipEvents") } """ Calendar-specific permissions for the CALENDAR page. All employees can view and work on events assigned to them. ACCESS_ALL = can see all events (not just own) COMPLETE_AND_INVOICE = can complete and invoice events (accessLevel >= 2) CREATE/UPDATE = can see prices when completing/invoicing (accessLevel >= 3, implies COMPLETE_AND_INVOICE) SKIP = can mark events as skipped DELETE = can delete events """ enum PermissionCalendar { ACCESS_ALL CREATE UPDATE COMPLETE_AND_INVOICE SKIP DELETE } enum Permission { READ UPDATE CREATE DELETE } """ Time-tracking-specific permissions. Always includes READ_OWN. READ_OWN = can view own time entries (default for all) READ_ALL = can view all users' time entries (implies READ_OWN) EDIT_OWN = can edit own time entries (implies READ_OWN) EDIT_ALL = can view and edit all users' time entries (implies all others) """ enum PermissionTimeTracking { READ_OWN READ_ALL EDIT_OWN EDIT_ALL } """ Per-event-type color overrides for the schedule view. Defaults are used for any key not explicitly set by the user. """ type ScheduleColors { visit: String! todo: String! meeting: String! maintenance: String! } enum VendorStatus { ACTIVE INACTIVE DELETED } type Vendor { id: SafeInt! companyId: SafeInt! firstName: String! lastName: String! companyName: String email: String! phone: String cell: String address: Address description: String isDeleted: Boolean! status: VendorStatus """ Type references the customer_types table (dynamic, company-specific). Use the raw integer ID to filter. """ type: SafeInt customerType: CustomerType vendorNumber: String registeredAt: DateTime updatedAt: DateTime } input VendorFilter { id: IntFilter isDeleted: Boolean status: VendorStatus firstName: StringFilter lastName: StringFilter companyName: StringFilter email: StringFilter city: StringFilter state: StringFilter AND: [VendorFilter!] OR: [VendorFilter!] NOT: VendorFilter } input VendorSort { id: SortOrder firstName: SortOrder lastName: SortOrder companyName: SortOrder } type AppEvent { id: SafeInt! companyId: SafeInt customerId: SafeInt type: AppEventType! objectId: SafeInt objectType: ObjectType! actorType: AppEventActorType! actorId: SafeInt data: JSON initiatedAt: DateTime! createdAt: DateTime } input AppEventSort { initiatedAt: SortOrder createdAt: SortOrder } input CreateCrewInput { name: String! notes: String color: String tags: [String!] } input UpdateCrewInput { name: String notes: String color: String tags: [String!] } """ A single row in a customer report. Each row represents one group determined by the selected dimensions. If none are selected the query returns a single aggregate row covering all customers. """ type CustomerReportRow { customerTypeId: SafeInt sourceId: SafeInt status: CustomerStatus isDeleted: Boolean country: String state: String city: String zip: String """Resolved customer type for this group.""" customerType: CustomerType """Resolved lead source for this group.""" source: CustomerSource _count: CustomerReportCountAggregate _sum: CustomerReportSumAggregate _avg: CustomerReportAvgAggregate _min: CustomerReportMinAggregate _max: CustomerReportMaxAggregate count: CustomerReportCountAggregate @deprecated(reason: "Use _count") sum: CustomerReportSumAggregate @deprecated(reason: "Use _sum") avg: CustomerReportAvgAggregate @deprecated(reason: "Use _avg") min: CustomerReportMinAggregate @deprecated(reason: "Use _min") max: CustomerReportMaxAggregate @deprecated(reason: "Use _max") } type CustomerReportCountAggregate { id: SafeInt } type CustomerReportSumAggregate { credit: Decimal } type CustomerReportAvgAggregate { credit: Decimal discountPercent: Decimal } type CustomerReportMinAggregate { credit: Decimal } type CustomerReportMaxAggregate { credit: Decimal } input CustomerReportOrderBy { customerTypeId: SortOrder sourceId: SortOrder status: SortOrder isDeleted: SortOrder country: SortOrder state: SortOrder city: SortOrder zip: SortOrder _count: CustomerReportCountOrderBy _sum: CustomerReportSumOrderBy _avg: CustomerReportAvgOrderBy _min: CustomerReportMinOrderBy _max: CustomerReportMaxOrderBy } input CustomerReportCountOrderBy { id: SortOrder } input CustomerReportSumOrderBy { credit: SortOrder } input CustomerReportAvgOrderBy { credit: SortOrder discountPercent: SortOrder } input CustomerReportMinOrderBy { credit: SortOrder } input CustomerReportMaxOrderBy { credit: SortOrder } type CustomerSource { id: SafeInt! name: String! isVisible: Boolean } """ Input for creating or updating a customer. All fields are optional for updates; provide at minimum fullName or firstName+lastName when creating. """ input CustomerInput { """Display name for the customer (e.g. company name or full name)""" fullName: String firstName: String lastName: String """Primary contact email""" email: String """Business or landline phone number.""" phone: String """ Mobile/cell phone number. Set this field to enable SMS for the customer. For US customers, this value is used directly for SMS. For international customers, ensure country is also set — cellFull (used for international SMS) is auto-computed from cell + country. Never set cellFull directly. """ cell: String fax: String address: AddressInput """Customer type ID (references customer_types table)""" customerTypeId: SafeInt """Customer source ID (references customer_sources table)""" sourceId: SafeInt description: String discountPercent: Decimal isReceivePhone: Boolean isReceiveEmail: Boolean isReceiveText: Boolean """CC email address 1""" ccEmail: String """CC email address 2""" ccEmail1: String """CC email address 3""" ccEmail2: String """CC email address 4""" ccEmail3: String """0 = taxable, 1 = exempt (see company settings)""" isTaxExempt: SafeInt """Email template ID to use for invoice delivery to this customer""" invoiceEmailTemplateId: SafeInt """SMS template ID to use for invoice delivery to this customer""" invoiceSmsTemplateId: SafeInt """SMS template ID to use for estimate delivery to this customer""" estimateSmsTemplateId: SafeInt """ Optional custom field values to set at creation time. Each fieldId must be a DynamicCustomField that belongs to this company and applies to customers. Unrecognised fieldIds throw BadRequest. Fields omitted here are seeded with empty/zero defaults. """ customFields: [CustomFieldInput!] """ Replace-set of tag names. Supplying this field replaces the current tag set; omitting it leaves tags untouched. """ tags: [String!] """ Same AutoChargePolicy enum as Customer.autoChargePolicy (INHERIT / DISABLED / USE_COMPANY). """ autoChargePolicy: AutoChargePolicy } type EstimateReportRow { status: EstimateStatus customerId: SafeInt date: Date isSent: Boolean customer: Customer _count: EstimateReportCountAggregate _sum: EstimateReportSumAggregate _avg: EstimateReportAvgAggregate _min: EstimateReportMinAggregate _max: EstimateReportMaxAggregate count: EstimateReportCountAggregate @deprecated(reason: "Use _count") sum: EstimateReportSumAggregate @deprecated(reason: "Use _sum") avg: EstimateReportAvgAggregate @deprecated(reason: "Use _avg") min: EstimateReportMinAggregate @deprecated(reason: "Use _min") max: EstimateReportMaxAggregate @deprecated(reason: "Use _max") } enum EstimateSentByType { EMAIL MAIL DROPPED_OFF NOT_SENT } enum EstimateLineItemEventStatus { """Not scheduled""" NO_EVENT """Scheduled and linked to event""" EVENT_CREATED """Manually marked as scheduled with no event""" EVENT_OVERRIDE """ Only currently used for line items that were created before October 1st """ UNKNOWN } input EstimateLineItemEventStatusFilter { equals: EstimateLineItemEventStatus not: EstimateLineItemEventStatus in: [EstimateLineItemEventStatus!] notIn: [EstimateLineItemEventStatus!] } input CreateEstimateInput { number: SafeInt purchaseOrder: String customerId: SafeInt! date: DateTime internalNotes: String title: String notes: String terms: String discountPercent: Decimal requestId: SafeInt tags: String """If null, this will inherit from the Company setting.""" payInFullEnabled: Boolean } input UpdateEstimateInput { number: SafeInt purchaseOrder: String customerId: SafeInt date: DateTime internalNotes: String title: String notes: String terms: String discountPercent: Decimal requestId: SafeInt tags: String """If null, this will inherit from the Company setting.""" payInFullEnabled: Boolean } input EstimateLineItemInput { name: String description: String date: DateTime price: Decimal quantity: Decimal taxPercent1: Decimal taxPercent2: Decimal itemId: SafeInt sortOrder: SafeInt propertyId: SafeInt } enum EstimateSendMethod { EMAIL MAIL SMS } enum EstimateSendStatus { QUEUED SENT ERROR } type EstimateSendResult { estimateId: SafeInt! status: EstimateSendStatus! errorMessage: String } type EstimateSendResults { results: [EstimateSendResult!]! } """ A category that can be assigned to an event (e.g. a TODO or Meeting category). """ type EventCategory { id: SafeInt! name: String! """The event type this category applies to (e.g. TODO, MEETING).""" type: EventType! companyId: SafeInt! } input EventCategoryFilter { id: IntFilter name: StringFilter type: EventTypeFilter isDeleted: Boolean } input EventCategorySort { id: SortOrder name: SortOrder type: SortOrder } type EventForm { id: SafeInt! eventId: SafeInt! formId: SafeInt formName: String submittedContent: String submittedSignature: String submittedByUserId: SafeInt createdAt: DateTime updatedAt: DateTime content: String! form: Form event: Event submittedByUser: User } enum FormEditorType { COMPANY_OWNER EMPLOYEES PROPERTY_OWNER ALL } type Form { id: SafeInt! name: String! content: String! editors: [FormEditorType!]! } """Deprecated. Use Event.users instead.""" type EventEmployee { id: SafeInt! eventId: SafeInt! employeeUserId: SafeInt! """ The user assigned to this event. May be null if employeeUserId is 0 in the database. """ user: User createdAt: DateTime! } input EventEmployeeFilter { employeeUserId: IntFilter } type TimeEntryTotals { pastTimeEntries: SafeInt! pastTimeEntrySeconds: SafeInt! runningTimers: SafeInt! runningTimerSeconds: SafeInt! totalSeconds: SafeInt! at: DateTime! } type DispatchNote { message: String! author: String! date: DateTime! } input DispatchNoteInput { message: String! date: DateTime } type EventReportRow { status: EventStatus type: EventType customerId: SafeInt crewId: SafeInt propertyId: SafeInt recurringEventId: SafeInt completedByUserId: SafeInt categoryId: SafeInt budgetedHours: Decimal isInvoiceable: Boolean """ The calendar year of the event's start date, in the company's timezone. Note: events created via automated/cron paths (e.g. QuickBooks sync, auto-bill) may reflect UTC instead. """ year: Int """ The calendar month (1–12) of the event's start date, in the company's timezone. Note: events created via automated/cron paths (e.g. QuickBooks sync, auto-bill) may reflect UTC instead. """ month: Int startDate: Date customer: Customer crew: Crew property: Property completedByUser: User _count: EventReportCountAggregate _sum: EventReportSumAggregate _avg: EventReportAvgAggregate _min: EventReportMinAggregate _max: EventReportMaxAggregate """@deprecated Use _count""" count: EventReportCountAggregate @deprecated(reason: "Use _count") """@deprecated Use _sum""" sum: EventReportSumAggregate @deprecated(reason: "Use _sum") """@deprecated Use _avg""" avg: EventReportAvgAggregate @deprecated(reason: "Use _avg") """@deprecated Use _min""" min: EventReportMinAggregate @deprecated(reason: "Use _min") """@deprecated Use _max""" max: EventReportMaxAggregate @deprecated(reason: "Use _max") } """ Subset of EventStatus valid on create. CLOSED, SKIPPED, and CANCELLED are not valid initial states. """ enum CreateEventStatus { OPEN WAITLISTED } input CreateEventLineItemInput { itemId: SafeInt! price: Decimal! quantity: Decimal! """ Budgeted hours for this service. Contributes to the event-level budgetedHours, which is computed as the sum of all line item budgetedHours for VISIT events. """ budgetedHours: Decimal """ Tax rate as a percentage (e.g. 8.5 for 8.5%). Optional — omit to create the line item without tax. If tax applies, use the lookupTaxRate query to resolve the correct rate for the customer / property / item combination. total is computed server-side as price x quantity x (1 + tax / 100). """ tax: Decimal description: String """Links this line item back to the source estimate line.""" estimateLineId: SafeInt } input CreateEventInput { """ The type of event. Determines which fields are required. - VISIT requires customerId, propertyId, and lineItems. - TODO and MEETING require only title and startDate; all other fields are optional. - MAINTENANCE requires equipmentId (title is auto-generated from equipment name + model). - WORK_REQUEST requires customerId and workRequestId. Line items may only be provided for VISIT. """ type: EventType! """ Required for all types except MAINTENANCE (auto-generated from equipment name + model). """ title: String """ Initial status. Only OPEN and WAITLISTED are valid on create. Defaults to OPEN. """ status: CreateEventStatus """ISO 8601 date (YYYY-MM-DD).""" startDate: LocalDate! """ If provided, the event has a specific time of day (hasTime = true). ISO 8601 time (HH:MM:SS). """ startTime: LocalTime """ Inclusive last day for multi-day events, or waitlist scheduling window end. Only valid when startTime is not set. Exact last active date for a multi-day event, inclusive for every event status. Null for a single-day event. ISO 8601 date (YYYY-MM-DD). """ endDate: LocalDate """ Scheduled duration in hours. Determines endTime when startTime is provided. Must not be set for VISIT events — budgetedHours on VISIT events is derived automatically as the sum of the line items' budgetedHours. Can be updated/prorated later based on time tracked. Defaults to 0 for non-VISIT types. """ budgetedHours: Decimal """Required for VISIT and WORK_REQUEST. Optional for TODO and MEETING.""" customerId: SafeInt """Required for VISIT. Optional for other types.""" propertyId: SafeInt """Required for MAINTENANCE only.""" equipmentId: SafeInt """ Required for WORK_REQUEST type. Optional for VISIT (links the visit to a work request without requiring the WORK_REQUEST type). """ workRequestId: SafeInt """ If provided, the linked estimate is automatically marked as ACCEPTED and estimate line items are linked to the created event line items. """ estimateId: SafeInt contractId: SafeInt crewId: SafeInt """User IDs to assign to this event.""" userIds: [SafeInt!] description: String categoryId: SafeInt """Freeform location label override (not a property lookup).""" locationName: String """Hex color string (e.g. '#FF0000'). Defaults to empty string.""" color: String isInvoiceable: Boolean tags: [String!] """ S3 temp file keys to attach as media. Obtain keys via the createUpload mutation: call createUpload, PUT the file to the returned uploadUrl, then pass the key here. """ mediaFileKeys: [String!] """MAINTENANCE type only. Defaults to 0.""" maintenanceCost: Decimal """ Form IDs to attach on creation. Forms can also be managed after creation via addEventForm / removeEventForm mutations. """ formIds: [SafeInt!] """ Required for VISIT. At least one item must be provided. Only valid for VISIT — other types must omit lineItems. total is computed server-side — do not send it. """ lineItems: [CreateEventLineItemInput!] """ Optional custom field values to set at creation time. Each fieldId must be a DynamicCustomField that belongs to this company and applies to events. Unrecognised fieldIds throw BadRequest. Fields omitted here are seeded with empty/zero defaults. """ customFields: [CustomFieldInput!] } """The series-level status of a recurring event, derived at query time.""" enum RecurringSeriesStatus { """Series is active and generating visits on schedule.""" ACTIVE """ Series is paused because today falls within its assigned off-season window. """ PAUSED """Series has been permanently cancelled.""" CANCELLED } """ A named, annually-recurring date range used to pause recurring event series. Off-seasons are company-wide templates (e.g. "Winter Break: Nov 1 – Feb 28"). No year is stored — the range repeats every year. """ type OffSeason { id: SafeInt! name: String! """Month the off-season starts (1–12).""" startMonth: Int! """Day of the month the off-season starts.""" startDay: Int! """Month the off-season ends (1–12).""" endMonth: Int! """Day of the month the off-season ends.""" endDay: Int! } input OffSeasonFilter { id: IntFilter name: StringFilter } input OffSeasonSort { id: SortOrder name: SortOrder startMonth: SortOrder startDay: SortOrder endMonth: SortOrder endDay: SortOrder } type RecurringEventUser { id: SafeInt! companyId: SafeInt! recurringEventId: SafeInt! userId: SafeInt! user: User! } type InvoiceReportRow { status: InvoiceStatus customerId: SafeInt propertyId: SafeInt date: Date isSent: Boolean source: String customer: Customer property: Property _count: InvoiceReportCountAggregate _sum: InvoiceReportSumAggregate _avg: InvoiceReportAvgAggregate _min: InvoiceReportMinAggregate _max: InvoiceReportMaxAggregate count: InvoiceReportCountAggregate @deprecated(reason: "Use _count") sum: InvoiceReportSumAggregate @deprecated(reason: "Use _sum") avg: InvoiceReportAvgAggregate @deprecated(reason: "Use _avg") min: InvoiceReportMinAggregate @deprecated(reason: "Use _min") max: InvoiceReportMaxAggregate @deprecated(reason: "Use _max") } """ QuickBooks item mapping sync state for an invoice line item or customer module onboarding. """ enum QuickbooksSyncStatus { """Item has been matched in QuickBooks but not yet confirmed""" MAPPING_PENDING """Item is fully mapped and confirmed in QuickBooks""" MAPPING_COMPLETE """Customer QB module onboarding has been initiated but not yet completed""" MODULE_MAPPING_PENDING """Customer QB module onboarding is complete""" MODULE_MAPPING_COMPLETE } enum InvoiceSendMethod { EMAIL SMS SNAIL_MAIL } input UpdateInvoiceLineItemInput { """ID of the service item from the catalog. Optional.""" itemId: SafeInt """Display name for this line item.""" name: String """Description for this line item.""" description: String """Unit price.""" price: Decimal """Quantity.""" quantity: Decimal """Primary tax percent (e.g. 8.5 for 8.5%).""" taxPercent1: Decimal """Secondary tax percent.""" taxPercent2: Decimal """Sort order for display.""" sortOrder: SafeInt """Link this line item to a specific property/asset.""" propertyId: SafeInt } input CreateInvoiceLineItemInput { """ID of the service item from the catalog. Optional.""" itemId: SafeInt """Display name for this line item.""" name: String! """Description for this line item. Defaults to empty string.""" description: String """Unit price.""" price: Decimal! """Quantity. Defaults to 1.""" quantity: Decimal """ Primary tax percent (e.g. 8.5 for 8.5%). Use the lookupTaxRate query to resolve the correct rate for the customer/item/property combination before submitting. Defaults to 0. """ taxPercent1: Decimal """Secondary tax percent. Defaults to 0.""" taxPercent2: Decimal """Sort order for display. Defaults to 0.""" sortOrder: SafeInt """Link this line item to a specific property/asset.""" propertyId: SafeInt } input CreateInvoiceInput { """ID of the customer this invoice is for.""" customerId: SafeInt! """Invoice date. ISO 8601 date (YYYY-MM-DD).""" date: LocalDate! """ Invoice-level discount percentage (e.g. 10 for 10%). Applied uniformly to all line items. Defaults to 0. """ discountPercent: Decimal """Optional title for the invoice.""" title: String """ Public notes shown on the invoice. If omitted, resolved from the customer's default notes, falling back to company settings. """ notes: String """ Payment terms shown on the invoice. If omitted, resolved from the customer's default terms, falling back to company settings. """ terms: String """Internal notes — not visible to the customer.""" internalNotes: String """Purchase order number.""" purchaseOrder: String """Property/asset to associate with this invoice.""" propertyId: SafeInt """ Line items for the invoice. At least one is required. Subtotals, taxes, and totals are computed server-side — do not send them. Company-level global items are automatically appended server-side. """ lineItems: [CreateInvoiceLineItemInput!]! """ Optional S3 temp keys (include temp/ prefix). Attached after create; failures are logged and do not fail the mutation. """ mediaFileKeys: [String!] """ Optional custom field values to set at creation time. Each fieldId must be a DynamicCustomField that belongs to this company and applies to invoices. Unrecognised fieldIds throw BadRequest. Fields omitted here are seeded with empty/zero defaults. """ customFields: [CustomFieldInput!] } input ItemInput { categoryId: SafeInt! type: ItemType! = SERVICE name: String! description: String! = "" price: Decimal! = "0" quantity: Decimal! = "1" budgetedHours: Decimal = "1" tax1: Decimal! = "0" tax2: Decimal! = "0" unit: String! = "" supplierId: SafeInt materialCost: Decimal! = "0" equipmentCost: Decimal! = "0" subcontractCost: Decimal! = "0" hasInventory: Boolean! = false wholesaleCost: Decimal! = "0" inventoryOnHand: SafeInt! = 0 markup: Decimal! = "0" markupType: ItemMarkupType! = NONE isGlobal: Boolean! = false isTaxable: Boolean! = true color: String rateChargedP4p: Decimal! = "0" } """Nearly the same as ItemInput except every field is optional.""" input ItemUpdateInput { categoryId: SafeInt type: ItemType name: String description: String price: Decimal quantity: Decimal budgetedHours: Decimal tax1: Decimal tax2: Decimal unit: String supplierId: SafeInt materialCost: Decimal equipmentCost: Decimal subcontractCost: Decimal hasInventory: Boolean wholesaleCost: Decimal inventoryOnHand: SafeInt markup: Decimal markupType: ItemMarkupType isGlobal: Boolean isTaxable: Boolean color: String rateChargedP4p: Decimal } type PaymentReportRow { date: Date method: PaymentMethod customerId: SafeInt customer: Customer isRefund: Boolean _count: PaymentReportCountAggregate _sum: PaymentReportSumAggregate _avg: PaymentReportAvgAggregate _min: PaymentReportMinAggregate _max: PaymentReportMaxAggregate count: PaymentReportCountAggregate @deprecated(reason: "Use _count") sum: PaymentReportSumAggregate @deprecated(reason: "Use _sum") avg: PaymentReportAvgAggregate @deprecated(reason: "Use _avg") min: PaymentReportMinAggregate @deprecated(reason: "Use _min") max: PaymentReportMaxAggregate @deprecated(reason: "Use _max") } enum PaymentPayeeType { CUSTOMER VENDOR EQUIPMENT EMPLOYEE } type PaymentPayee { type: PaymentPayeeType! id: SafeInt! name: String! } input RouteReportOrderBy { crewId: SortOrder equipmentId: SortOrder date: SortOrder _count: RouteReportCountOrderBy _sum: RouteReportSumOrderBy _avg: RouteReportAvgOrderBy _min: RouteReportMinOrderBy _max: RouteReportMaxOrderBy count: RouteReportCountOrderBy @deprecated(reason: "Use _count") sum: RouteReportSumOrderBy @deprecated(reason: "Use _sum") avg: RouteReportAvgOrderBy @deprecated(reason: "Use _avg") min: RouteReportMinOrderBy @deprecated(reason: "Use _min") max: RouteReportMaxOrderBy @deprecated(reason: "Use _max") } input RouteReportCountOrderBy { id: SortOrder } input RouteReportSumOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder } input RouteReportAvgOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder } input RouteReportMinOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder date: SortOrder } input RouteReportMaxOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder date: SortOrder } type RouteReportCountAggregate { """Total number of routes in this group.""" id: SafeInt! } type RouteReportSumAggregate { """Total miles driven across all routes in this group.""" totalMiles: Decimal """Total estimated fuel cost across all routes in this group.""" totalCost: Decimal """Total windshield time in seconds across all routes in this group.""" totalDriveSeconds: SafeInt """Total number of route stops across all routes in this group.""" totalStops: SafeInt } type RouteReportAvgAggregate { """Average miles per route.""" totalMiles: Float """Average estimated fuel cost per route.""" totalCost: Float """Average windshield time in seconds per route.""" totalDriveSeconds: Float """Average number of stops per route.""" totalStops: Float } type RouteReportMinAggregate { totalMiles: Decimal totalCost: Decimal totalDriveSeconds: SafeInt totalStops: SafeInt date: LocalDate } type RouteReportMaxAggregate { totalMiles: Decimal totalCost: Decimal totalDriveSeconds: SafeInt totalStops: SafeInt date: LocalDate } """ A single row in a route report. Each row represents one group determined by the selected dimensions (crewId, equipmentId, date). If none of those fields are selected the query returns a single aggregate row covering all routes. """ type RouteReportRow { crewId: SafeInt equipmentId: SafeInt """ The route date for this group. Null when grouping across multiple dates. """ date: LocalDate """ Crew for this group. Null when crewId is not selected or no crew is set. """ crew: Crew """ Equipment (vehicle) for this group. Null when equipmentId is not selected. """ equipment: Equipment _count: RouteReportCountAggregate _sum: RouteReportSumAggregate _avg: RouteReportAvgAggregate _min: RouteReportMinAggregate _max: RouteReportMaxAggregate count: RouteReportCountAggregate @deprecated(reason: "Use _count") sum: RouteReportSumAggregate @deprecated(reason: "Use _sum") avg: RouteReportAvgAggregate @deprecated(reason: "Use _avg") min: RouteReportMinAggregate @deprecated(reason: "Use _min") max: RouteReportMaxAggregate @deprecated(reason: "Use _max") } """A resolved coordinate with optional human-readable address.""" type Coordinate { longitude: Float! latitude: Float! """ Human-readable address for this location, if one was provided when the route was saved. """ address: String } type Route { id: SafeInt! """ Opaque key shared by every route in the same recurring series. Null for one-off (non-series) routes. """ seriesKey: String crewId: SafeInt equipmentId: SafeInt costPerGallon: Decimal milesPerGallon: Decimal date: LocalDate """ Company-local departure time saved on the route, as `HH:mm:ss` — the value passed as `departAt` when the route was created or updated. Null when none was set explicitly; see `departAt` for the instant the route actually departs. """ departTime: LocalTime """ The UTC instant this route departs the depot. Resolved the same way the routing provider and the arrival estimates resolve it: the saved `departTime` when there is one, else the earliest scheduled visit on the route, else 08:00 company-local. Null only when the route has no date. This is the timestamp; `departTime` is the wall clock it came from. """ departAt: DateTime totalMiles: Decimal """Total route distance in meters (derived from totalMiles).""" totalMeters: Int totalCost: Decimal totalTime: String """ Total windshield time in seconds — every stop's drive leg plus the return to the depot. Null if the routing provider was unavailable when the route was saved. """ totalDriveSeconds: Int """ Total planned on-site work time in hours — sum of budgetedHours across all route stops. Zero if no stops have budgeted hours set. Use alongside totalDriveSeconds to compute the windshield-to-on-site ratio. """ budgetedHours: Float! """Number of stops — resolved via live Prisma count, always accurate.""" routeStopCount: Int! routeStops: [RouteStop!]! """ Crew assigned to this route. Null if crewId is unset or crew is not selected. """ crew: Crew """ Equipment (vehicle) assigned to this route. Null if equipmentId is unset or equipment is not selected. """ equipment: Equipment """ Turn-by-turn directions for this route in stop order (depot → stops → depot). Null when the route has no stops with coordinates, or if the directions API is unavailable. """ directions: DirectionsResult """ Directions leg for the return trip from the last stop back to the depot. Null when no return leg data has been saved, or the route has no stops. """ returnLeg: DirectionsLeg """ Depot start override for this route — the lat/lng (and address if provided) of the custom start location. """ startLocation: Coordinate """ Depot end override for this route — the lat/lng (and address if provided) of the custom end location. """ endLocation: Coordinate } type RouteStop { id: SafeInt! routeId: SafeInt! eventId: SafeInt! """The route this stop belongs to. Only fetched when selected.""" route: Route stopOrder: Int! routeDate: LocalDate! """ Drive time in seconds from the previous stop (or depot start for stop 0). Null if directions API data unavailable. """ driveTimeSeconds: Int """ Drive distance in meters from the previous stop (or depot start for stop 0). Null if directions API data unavailable. """ driveDistanceMeters: Int """Drive distance in miles. Null if driveDistanceMeters is null.""" driveDistanceMiles: Float """ Estimated clock arrival at this stop. Only meaningful when departAt was known at save time. """ estimatedArrivalAt: DateTime """ Estimated departure from this stop (estimatedArrivalAt + event duration). Null if duration unknown. """ estimatedDepartureAt: DateTime event: Event """ Full directions leg for the drive from the previous stop (or depot) to this stop. Composed from cached DB columns (rs_leg_steps, driveDistanceMeters, driveTimeSeconds). Null if directions data has not been saved for this route. """ leg: DirectionsLeg } """ Routing provider to use for a request. Absent means the provider this deployment is configured with. Routing results are per-request and cached per provider, so choosing one here changes nothing that other requests can see. Geocoding is deliberately not selectable: it writes coordinates back to shared property and company rows. """ enum RoutingProvider { MAPBOX HERE } """ Result of a deleteRoute mutation — how many routes were deleted (0 or 1 for a single route, or more when andFutureRoutesInSeries is used). """ type DeleteRouteResult { deletedCount: Int! } """Input for createRoute.""" input CreateRouteInput { """The route date (YYYY-MM-DD in the company's timezone)""" date: LocalDate! """ Event IDs in the order they should be visited (stopOrder 0 upward). Pass `optimize: true` on the mutation to have the provider choose the order instead. Every id must belong to the company. """ eventIds: [SafeInt!]! """ Crew to assign to this route (and its events). Omit to derive from the majority crew of eventIds; null to explicitly clear. """ crewId: SafeInt """ Departure time (HH:MM:SS, company-local). Omit to derive from the earliest event start time. """ departAt: LocalTime """ Depot start location override. Omit to use the company's configured route start location. """ startLocation: CoordinateInput """ Depot end location override. Omit to use the company's configured route end location. """ endLocation: CoordinateInput } """ Input for updateRoute. All fields except eventIds are optional — omitted fields keep their current values. """ input UpdateRouteInput { """ New ordered list of event IDs. Replaces the current stop list outright, so an event left out of the list is removed from the route. Omit the field entirely to keep the stops as they are. """ eventIds: [SafeInt!] """Crew override. Omit to keep existing; null to explicitly clear.""" crewId: SafeInt """ Departure time override (HH:MM:SS, company-local). Omit to keep existing; null to clear (derive from event times). """ departAt: LocalTime """ Depot start location override. Omit to keep existing; null to clear (use company defaults). """ startLocation: CoordinateInput """ Depot end location override. Omit to keep existing; null to clear (use company defaults). """ endLocation: CoordinateInput } input RouteFilter { id: IntFilter crewId: IntFilter equipmentId: IntFilter date: DateFilter stops: RouteStopFilter AND: [RouteFilter!] OR: [RouteFilter!] NOT: RouteFilter } input RouteSort { id: SortOrder date: SortOrder } input RouteStopFilter { id: IntFilter routeId: IntFilter eventId: IntFilter routeDate: DateFilter event: EventFilter AND: [RouteStopFilter!] OR: [RouteStopFilter!] NOT: RouteStopFilter } input RouteStopSort { id: SortOrder stopOrder: SortOrder routeDate: SortOrder } """A [longitude, latitude] coordinate pair.""" input CoordinateInput { longitude: Float! latitude: Float! """ Optional label (e.g. event ID) echoed back on the matching leg, so a caller can map results to its own records. Never sent to the routing provider. """ label: String """ Human-readable address for this location (stored so it can be reloaded without reverse-geocoding). """ address: String } """Drive time and distance for an ordered set of waypoints.""" type DirectionsResult { """Total route distance in meters.""" totalMeters: Int! """Total route distance in miles.""" totalMiles: Float! """Total route duration in seconds.""" totalDurationSeconds: Int! """Total route duration formatted as HH:MM.""" totalTime: String! """ Estimated fuel cost (totalMiles * costPerGallon / milesPerGallon). Null if not provided. """ totalCost: Float """ Detailed breakdown of the route into legs. Each leg represents the journey between two consecutive waypoints (e.g., from the depot to the first stop, or between two stops). """ legs: [DirectionsLeg!]! """ Full route geometry as an encoded polyline (Google polyline format, precision 5). Decode with e.g. @mapbox/polyline. Null when unavailable. """ geometry: String } """A turn-by-turn maneuver step within a route leg.""" type DirectionsStep { """Human-readable navigation instruction.""" instruction: String! """ Maneuver type (e.g. 'turn', 'depart', 'arrive', 'merge', 'roundabout'). """ maneuverType: String! """ Maneuver modifier (e.g. 'right', 'left', 'slight right'). Null for depart/arrive. """ maneuverModifier: String """Step distance in meters.""" distanceMeters: Int! """Step duration in seconds.""" durationSeconds: Int! """ Step geometry as an encoded polyline (Google polyline format, precision 5). Null when unavailable. """ geometry: String } """A leg of a route between two consecutive waypoints.""" type DirectionsLeg { """ Label of the waypoint this leg departs from (from the input coordinates label, if provided). """ startLabel: String """ Label of the waypoint this leg arrives at (from the input coordinates label, if provided). """ endLabel: String """Leg distance in meters.""" distanceMeters: Int! """Leg distance in miles.""" distanceMiles: Float! """Leg duration in seconds.""" durationSeconds: Int! """ Detailed turn-by-turn navigation instructions (maneuvers) for this leg. """ steps: [DirectionsStep!]! } """ Result of an optimize route call — events in the optimized order, plus route metrics taken from a directions call rather than the optimizer. """ type OptimizeRouteResult { """ Events in the optimized order. Events without resolvable coordinates are excluded. """ orderedEvents: [Event!]! """Total route distance in meters.""" totalMeters: Int! """Total route distance in miles.""" totalMiles: Float! """Total route duration in seconds.""" totalDurationSeconds: Int! """Total route duration formatted as HH:MM.""" totalTime: String! """ Detailed breakdown of the route into legs, including turn-by-turn maneuvers. Empty array if directions call fails or if there are fewer than 2 waypoints. """ legs: [DirectionsLeg!]! """ Full optimized route geometry as an encoded polyline (Google polyline format, precision 5). Null if unavailable. """ geometry: String } """ Nearest event on the same day, ranked by drive time (or crow-flies in fastMode). """ type NearbyEvent { eventId: SafeInt! """ Null if the resolver only loaded a partial Event select and the full relation was not fetched. """ event: Event """Crow-flies distance in meters.""" distanceMeters: Int! """Crow-flies distance in miles.""" distanceMiles: Float! """Actual drive time in seconds. Null in fastMode or on API failure.""" driveTimeSeconds: Int """Actual drive distance in meters. Null in fastMode or on API failure.""" driveDistanceMeters: Int """Actual drive distance in miles. Null in fastMode or on API failure.""" driveDistanceMiles: Float """ The route stop for this event on this day, if it's already assigned to a route. Null if unrouted. """ routeStop: RouteStop } """Cheapest insertion point for a new stop on an existing route.""" type RouteInsertionSuggestion { """The ID of the route this insertion suggestion is for.""" routeId: SafeInt! route: Route """ The 0-based index of the existing stop after which the new stop should be inserted. 0 means it should be inserted at the very beginning (before stop 1), and a value equal to the current stop count means it should be appended at the end. """ insertAfterStop: Int! """ The additional time (in seconds) added to the total route duration by inserting this new stop. """ extraSeconds: Int! """ The additional distance (in meters) added to the total route distance by inserting this new stop. """ extraMeters: Int! """Additional miles.""" extraMiles: Float! """ Directions for just the affected segment: the stop before the insertion point, the new location, and the stop after (1–2 legs). Only fetched when selected. Null if surrounding stops lack coordinates or directions API is unavailable. """ patchDirections: DirectionsResult } """Result container for the routeSuggestions query.""" type RouteInsertionSuggestions { """ The top 3 nearest events on the same day, ranked by drive time (or crow-flies distance if fastMode is enabled). """ nearbyEvents: [NearbyEvent!]! """Per-route insertion cost, ranked by extraSeconds ascending.""" routes: [RouteInsertionSuggestion!]! } """A single day ranked by cost of inserting a new stop.""" type SchedulingDateSuggestion { """The date being evaluated.""" date: LocalDate! """Per-route insertion costs for this day. Empty if no routes exist.""" routes: [RouteInsertionSuggestion!]! """ The nearest events on this day (with or without existing routes), ranked by drive time from the candidate location (or crow-flies distance if fastMode is enabled). """ nearbyEvents: [NearbyEvent!]! """ The additional time (in seconds) required to 'thread' the candidate location between the two most optimal nearby events on this day. Calculated as the marginal cost: driveTime(Event1 to Location) + driveTime(Location to Event2) - driveTime(Event1 to Event2). Null if fewer than 2 nearby events have coordinates. """ pairInsertionSeconds: Int """ The additional distance (in meters) required to 'thread' the candidate location between the two most optimal nearby events. Null if fewer than 2 nearby events have coordinates. """ pairInsertionMeters: Int """pairInsertionMeters in miles.""" pairInsertionMiles: Float """ Directions between the location and the two nearest events on this day (location → nearest → 2nd nearest). Only fetched when selected. Null if fewer than one nearby event has coords or directions API is unavailable. """ patchDirections: DirectionsResult """ Transit cost from company depot to this location (baseline for a new solo route). """ depotSeconds: Int! """Depot-to-location distance in meters.""" depotMeters: Int! """depotMeters in miles.""" depotMiles: Float! } type Sms { id: SafeInt! customerId: SafeInt customer: Customer counterPartyUserId: SafeInt counterPartyUser: User sentByUserId: SafeInt sentByUser: User pcsId: SafeInt! sid: String! """The phone number this message was sent to""" to: String! """The phone number this message was sent from""" from: String! """Message body""" text: String! notes: String status: SmsStatus! direction: SmsDirection! segments: SafeInt! isViewed: Boolean! isDone: Boolean! stripeStatus: SmsStripeStatus! """ Canonical conversation identifier — sorted concat of to/from phone numbers """ conversationId: String """The other party's phone number (from for inbound, to for outbound)""" counterparty: String createdAt: DateTime! updatedAt: DateTime! sentAt: DateTime! } input SmsFilter { id: IntFilter customerId: IntFilter counterPartyUserId: IntFilter counterPartyUser: UserFilter sentByUserId: IntFilter sentByUser: UserFilter direction: SmsDirection status: SmsStatusFilter stripeStatus: SmsStripeStatusFilter isViewed: Boolean isDone: Boolean createdAt: DateTimeFilter sentAt: DateTimeFilter conversationId: String counterparty: StringFilter AND: [SmsFilter!] OR: [SmsFilter!] NOT: SmsFilter } input SmsSort { id: SortOrder createdAt: SortOrder sentAt: SortOrder customerId: SortOrder direction: SortOrder } """Why the customer cancelled their subscription.""" input CancellationFeedbackInput { cancellationReason: CancellationReason! cancellationReasonCustom: String highlightEaseOfUse: Boolean highlightCustomerSupport: Boolean highlightGreatCommunity: Boolean highlightValueForPrice: Boolean highlightGreatOnboarding: Boolean highlightOther: Boolean satisfactionHighlightCustom: String } type CompanyStripeSetupIntent { clientSecret: String! setupIntentId: String! customerSessionClientSecret: String! } input BillingAddressInput { postalCode: String! country: String! } type StripeSubscription { subscriptionId: String! status: String! currentPeriodEnd: DateTime! } type StripeSubscriptionPhase { tier: Tier! planPriceId: String! planFrequency: BillingInterval! additionalUsers: SafeInt! startDate: DateTime! endDate: DateTime! } type RetryPaymentResult { success: Boolean! invoiceId: String error: String declineCode: String } """How long a promo code's discount applies.""" enum PromoCodeDuration { """Applies to the first invoice only.""" ONCE """Applies for a fixed number of months (see durationInMonths).""" REPEATING """Applies to every invoice for the life of the subscription.""" FOREVER } """ Details of the discount a promo code applies, for display during checkout. """ type PromoCodeDetails { """ Normalized customer-facing code as stored in Stripe (canonical casing). """ code: String! """ Human-readable summary of the discount (e.g. 'First month free', '20% off for 3 months'). """ description: String! """ Percent off the subtotal for percentage codes. Null for fixed-amount codes. """ percentOff: Float """ Fixed amount off in cents for fixed-amount codes. Null for percentage codes. """ amountOffCents: SafeInt """ Uppercase ISO currency for amountOffCents (e.g. 'USD'). Null for percentage codes. """ currency: String """ Whether the discount applies once, repeats for a number of months, or forever. """ duration: PromoCodeDuration! """ Number of months the discount repeats when duration is REPEATING. Null otherwise. """ durationInMonths: Int } """ Why a promo code could not be applied at checkout (set only when isValid is false). """ enum PromoCodeErrorReason { """The code is blank, unknown, or no longer active.""" INVALID """ The code is real but can't be redeemed for this account or plan (e.g. first-time-customers-only, usage limit, minimum amount). """ NOT_APPLICABLE """ Validation couldn't complete (Stripe unreachable, or no checkout context yet); the code may still be valid on retry. """ UNAVAILABLE } """ Why a promo code could not be applied during checkout. Present only when isValid is false. """ type PromoCodeError { """Machine-readable failure reason for branching.""" reason: PromoCodeErrorReason! """Short, customer-facing explanation for display.""" message: String! } """Result of validating a promo code against a plan during checkout.""" type PromoCodeValidation { """ Whether the promo code is valid and can be applied to the selected plan. """ isValid: Boolean! """ First-invoice total in cents before the promo code is applied. Null when the code is invalid. """ originalTotalCents: SafeInt """ First-invoice total in cents after the promo code is applied (e.g. 0 for a one-month-free referral code). Null when the code is invalid. NOTE: reflects the first invoice only — for REPEATING/FOREVER discounts use details.duration to convey that the discount recurs. """ discountedTotalCents: SafeInt """ Discount details for display (percentage/amount, duration, description). Present only when isValid is true. """ details: PromoCodeDetails """ Failure reason and display message. Present only when isValid is false. """ error: PromoCodeError } enum TagObjectType { ASSET CREW CUSTOMER EVENT RECURRING_EVENT VENDOR } type Tag { id: SafeInt! tag: String! createdAt: DateTime updatedAt: DateTime objectTagCount(type: TagObjectType): SafeInt! """ True if this tag is attached to at least one event (any status). hasEvents, hasCustomers, and hasRecurringEvents share a single batched EXISTS-style query per request — selecting all three costs the same as one. """ hasEvents: Boolean! """True if this tag is attached to at least one customer.""" hasCustomers: Boolean! """True if this tag is attached to at least one recurring event.""" hasRecurringEvents: Boolean! eventCount(where: EventFilter): SafeInt! events(where: EventFilter, orderBy: [EventSort!], take: SafeInt, skip: SafeInt): [Event!]! customerCount(where: CustomerFilter): SafeInt! customers(where: CustomerFilter, orderBy: [CustomerSort!], take: SafeInt, skip: SafeInt): [Customer!]! recurringEventCount(where: RecurringEventFilter): SafeInt! recurringEvents(where: RecurringEventFilter, orderBy: [RecurringEventSort!], take: SafeInt, skip: SafeInt): [RecurringEvent!]! } input TagFilter { tag: StringFilter """Filter to tags that have at least one object tag of the given type.""" hasEvents: Boolean """Filter to tags that are attached to at least one customer.""" hasCustomers: Boolean """Filter to tags that are attached to at least one recurring event.""" hasRecurringEvents: Boolean } input TagOrderBy { id: SortOrder tag: SortOrder createdAt: SortOrder updatedAt: SortOrder objectTagCount: SortOrder } enum PropertyDataSet { SAN_FRANCISCO_USA CHICAGO_USA PORTLAND_USA LONDON_UK } type TimeEntryReportRow { userId: SafeInt categoryId: SafeInt eventId: SafeInt customerId: SafeInt propertyId: SafeInt user: User category: TimeEntryCategory event: Event customer: Customer property: Property _count: TimeEntryReportCountAggregate _sum: TimeEntryReportSumAggregate _avg: TimeEntryReportAvgAggregate _min: TimeEntryReportMinAggregate _max: TimeEntryReportMaxAggregate count: TimeEntryReportCountAggregate @deprecated(reason: "Use _count") sum: TimeEntryReportSumAggregate @deprecated(reason: "Use _sum") avg: TimeEntryReportAvgAggregate @deprecated(reason: "Use _avg") min: TimeEntryReportMinAggregate @deprecated(reason: "Use _min") max: TimeEntryReportMaxAggregate @deprecated(reason: "Use _max") } enum TimeEntryType { GLOBAL_TIMER EVENT_TIMER } type TimeEntryCategory { id: SafeInt! name: String! color: String! isDeletable: Boolean! } input TimeEntryCategorySort { name: SortOrder id: SortOrder } type TimeEntry { id: SafeInt! type: TimeEntryType! userId: SafeInt! user: User! """Not null for global timers. Null for event timers.""" categoryId: SafeInt category: TimeEntryCategory """Not null for event timers. Null for global timers.""" eventId: SafeInt event: Event """Optional input for event timers""" customerId: SafeInt customer: Customer """Optional input for event timers""" propertyId: SafeInt property: Property reviewerUserId: SafeInt paymentId: SafeInt bonusPaymentId: SafeInt """Timestamp that timer started""" startedAt: DateTime! """Timestamp that timer ended""" endedAt: DateTime """Total seconds spent between start and end times""" totalSeconds: SafeInt! acceptedAt: DateTime deniedAt: DateTime hourlyRate: Decimal paymentAmount: Decimal bonusPaymentAmount: Decimal! location: Location notes: String } enum ClockInOutType { CLOCK_IN CLOCK_OUT } type ClockInOutAction { id: SafeInt! type: ClockInOutType! userId: SafeInt! user: User! """UTC timestamp that the user clocked in""" createdAt: DateTime! location: Location } input TimeEntryInput { """Not null for global timers. Null for event timers.""" categoryId: SafeInt """Not null for event timers. Null for global timers.""" eventId: SafeInt """Optional input for event timers""" customerId: SafeInt """Optional input for event timers""" propertyId: SafeInt """Timestamp that timer started""" startedAt: DateTime! """Timestamp that timer ended""" endedAt: DateTime! location: LocationInput notes: String userId: SafeInt hourlyRate: Decimal } input UpdateTimeEntryInput { """Timestamp that timer started""" startedAt: DateTime """Timestamp that timer ended""" endedAt: DateTime notes: String hourlyRate: Decimal } input TimeEntrySort { timeStart: SortOrder timeEnd: SortOrder id: SortOrder } input TimeEntryFilter { id: IntFilter userId: IntFilter categoryId: IntFilter eventId: IntFilter customerId: IntFilter propertyId: IntFilter reviewerUserId: IntFilter paymentId: IntFilter bonusPaymentId: IntFilter startedAt: DateTimeFilter endedAt: DateTimeFilter totalSeconds: IntFilter acceptedAt: DateTimeFilter deniedAt: DateTimeFilter hourlyRate: DecimalFilter paymentAmount: DecimalFilter bonusPaymentAmount: DecimalFilter notes: String user: UserFilter isRunning: Boolean } enum MockTwilioError { PN_NOT_PROVIDED PN_NOT_WITH_ISV PN_NOT_TOLLFREE PN_STATUS_IS_NOT_IN_USE NOTIFICATION_EMAIL_NOT_PROVIDED VERIFICATION_NOT_RESUBMITTABLE TRIAL_ACCOUNT GENERIC } """ https://www.twilio.com/docs/messaging/compliance/toll-free/compliance-embeddable-onboarding#initialize-complianceinquiry-api-for-toll-free-verification """ type ComplianceInquiryResponse { data: TwilioComplianceInquiry error: ComplianceInquiryError } type TwilioComplianceInquiry { id: SafeInt! companyId: SafeInt! subAccountSid: String! phoneNumber: String! """ A reference for your Toll-free verification Inquiry in Twilio. This value will be used to initialize the client component. """ inquiryId: String """ An ephemeral session token for your Compliance Inquiry in Twilio. This value will be used to initialize the client component. Expires in 1440 minutes from creation. """ inquirySessionToken: String sessionTokenUpdated: DateTime """A reference for your Toll-free verification resource in Twilio.""" registrationId: String url: String createdAt: DateTime updatedAt: DateTime } type ComplianceInquiryError { status: Int! message: String! code: Int moreInfo: String } """ See https://www.twilio.com/docs/messaging/compliance/toll-free/api-onboarding#create-a-tfv-request """ type TwilioVerification { sid: String accountSid: String customerProfileSid: String trustProductSid: String dateCreated: DateTime dateUpdated: DateTime regulatedItemSid: String businessName: String businessStreetAddress: String businessStreetAddress2: String businessCity: String businessStateProvinceRegion: String businessPostalCode: String businessCountry: String businessWebsite: String businessContactFirstName: String businessContactLastName: String businessContactEmail: String businessContactPhone: String notificationEmail: String useCaseCategories: [String] useCaseSummary: String productionMessageSample: String optInImageUrls: [String] optInType: String messageVolume: String additionalInformation: String tollfreePhoneNumberSid: String tollfreePhoneNumber: String status: String url: String rejectionReason: String rejectionReasons: [TwilioRejectionReason] errorCode: Int editExpiration: DateTime editAllowed: Boolean businessRegistrationNumber: String businessRegistrationAuthority: String businessRegistrationCountry: String businessType: String businessRegistrationPhoneNumber: String doingBusinessAs: String optInConfirmationMessage: String helpMessageSample: String privacyPolicyUrl: String termsAndConditionsUrl: String ageGatedContent: String optInKeywords: String } type TwilioRejectionReason { code: Int reason: String } type Upsell { id: SafeInt! companyId: SafeInt! name: String! emailTemplate: SafeInt! textTemplate: SafeInt! emailActive: Boolean! textActive: Boolean! createdAt: DateTime updatedAt: DateTime reviewRequired: Boolean! } type UpsellHistory { id: SafeInt! upsellId: SafeInt! companyId: SafeInt! customerId: SafeInt! senderUserId: SafeInt! eventId: SafeInt! sentTime: BigInt! name: String! wasSent: Boolean! upsell: Upsell customer: Customer senderUser: User event: Event } input UpsellFilter { id: IntFilter name: StringFilter reviewRequired: Boolean emailActive: Boolean textActive: Boolean } input UpsellSort { id: SortOrder name: SortOrder createdAt: SortOrder } input SubmitUpsellInput { upsellId: SafeInt! eventId: SafeInt! customerId: SafeInt! } type UserDayOverride { id: SafeInt! userId: SafeInt! date: Date! """ Available hours for this user on this date. Set to 0 to mark the user as unavailable. """ capacityHours: Decimal! createdAt: DateTime! updatedAt: DateTime! } input UserDayOverrideFilter { id: IntFilter userId: IntFilter date: DateFilter capacityHours: DecimalFilter AND: [UserDayOverrideFilter!] OR: [UserDayOverrideFilter!] NOT: UserDayOverrideFilter } input UserDayOverrideSort { id: SortOrder userId: SortOrder date: SortOrder capacityHours: SortOrder } input ScheduleColorsInput { visit: String todo: String meeting: String maintenance: String } input SetScheduleColorsInput { scheduleColors: ScheduleColorsInput! } input CreateUserInput { email: String! password: String! firstName: String! lastName: String! role: UserRole = EMPLOYEE hireDate: Date birthDate: Date } """ Tax rate breakdown returned by the ZipTax integration. All rates are expressed as percentages (e.g. 8.25, not 0.0825). """ type TaxRateLookupResult { """County tax rate as a percentage""" county: Decimal! """Combined city + district tax rate as a percentage""" city: Decimal! """State tax rate as a percentage""" state: Decimal! """Total combined tax rate (county + city + state) as a percentage""" totalTax: Decimal! """ZipTax response code — 100 = success, 110 = no results, etc.""" statusCode: Int """Human-readable message corresponding to statusCode""" statusMessage: String """ True when the result was served from the DB cache (no external API call made) """ fromCache: Boolean! }