openapi: 3.1.0
info:
  title: PropFlow Work Order Updates API
  version: 1.0.0-draft
  summary: A field app tells PropFlow when a maintenance tech is on the way, has arrived, and has finished.
  description: |
    A field app (for example Orca) posts one work order update each time a maintenance job changes
    status. PropFlow (Clara) texts the resident, adds the tech's notes and photos to the
    work order, and closes it in the customer's property management system (PMS). Every text and
    PMS write follows that customer's maintenance mode in PropFlow.

    **Send.** `POST /api/work-order-updates`, `Content-Type: application/json`, one update per request:
    `{ id, time, work_order, status, ... }`. `id` is unique per update and the same on every retry (a UUID works).
    `time` is when it happened, not when you sent it. Unknown fields are ignored.

    **Status.** One of `on_the_way`, `arrived`, `completed`. Extras ride on the update they belong to:
    `eta` with `on_the_way`; `notes`, `photos` and `labor_minutes` with `completed`. All extras are optional.
    One message with a status field is how Dynamics 365 Field Service, Uber and ServiceChannel report a visit,
    so the shape is familiar.

    **Work order.** Every update names the work order as the PMS knows it:
    `work_order { pms_account_id, pms_property_id, pms_work_order_id }`. There is no technician
    field: the work order in the PMS already names who is assigned.

    **Credential.** PropFlow makes one secret per partner and customer and shares it with your
    engineer out of band. Send it on every update as `Authorization: Bearer <secret>`. The secret
    decides which customer an update belongs to; nothing in the body can change that.

    **Replies.** Only three:
    - `202`: accepted. A repeat of an `id` you already sent also gets `202`, and nothing happens twice.
    - `401`: missing, bad or disabled credential.
    - `400`: anything else wrong; the body says what.
    If you get no reply, send the same update again with the same `id`.

servers:
  - url: https://propflowai.co
security:
  - bearer: []
paths:
  /api/work-order-updates:
    post:
      operationId: postWorkOrderUpdate
      summary: Send one work order update
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WorkOrderUpdate' }
            examples:
              on_the_way:
                summary: The tech left for the job
                value:
                  id: 88cdca95-2a99-4f1a-82c4-68d9eba0f63c
                  time: '2026-10-29T19:28:00-06:00'
                  work_order: { pms_account_id: westernslopepm, pms_property_id: '112', pms_work_order_id: '48213' }
                  status: on_the_way
                  eta: '2026-10-29T19:45:00-06:00'
              arrived:
                summary: The tech is at the unit
                value:
                  id: 0b7e2c1a-6f4d-4b8e-9a3c-5d2f1e8b7a60
                  time: '2026-10-29T19:44:00-06:00'
                  work_order: { pms_account_id: westernslopepm, pms_property_id: '112', pms_work_order_id: '48213' }
                  status: arrived
              completed:
                summary: The tech says nothing is left to do
                value:
                  id: 5c9d3e7f-1a2b-4c6d-8e0f-a1b2c3d4e5f6
                  time: '2026-10-29T20:31:00-06:00'
                  work_order: { pms_account_id: westernslopepm, pms_property_id: '112', pms_work_order_id: '48213' }
                  status: completed
                  notes: Replaced the kitchen faucet cartridge; no more drip.
                  photos:
                    - 'https://files.example-fieldapp.com/p/7d1e.jpg'
                  labor_minutes: 52
      responses:
        '202':
          description: Accepted. A repeat of an `id` already received also gets 202, and nothing happens twice.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Accepted' }
              example: { id: 88cdca95-2a99-4f1a-82c4-68d9eba0f63c }
        '400':
          description: Anything else wrong (not JSON, an unknown status, a missing or malformed field, a work order not in this customer). The body says what.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: 'work_order.pms_work_order_id is required' }
        '401':
          description: Missing, bad or disabled credential.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: 'bad or disabled credential' }
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: 'The per-customer secret, sent as `Authorization: Bearer <secret>`.'
  schemas:
    WorkOrderUpdate:
      type: object
      required: [id, time, work_order, status]
      properties:
        id: { type: string, minLength: 1, description: Unique per update; the same on every retry. A UUID works. }
        time: { type: string, format: date-time, description: When it happened, with an offset. }
        work_order: { $ref: '#/components/schemas/WorkOrderRef' }
        status:
          type: string
          enum: [on_the_way, arrived, completed]
          description: |
            `on_the_way`: the tech left for the job. `arrived`: the tech is at the unit.
            `completed`: the tech says nothing is left to do.
        eta: { type: string, format: date-time, description: 'Optional, with `on_the_way`. When the tech expects to arrive.' }
        notes: { type: string, description: 'Optional, with `completed`. The tech''s notes.' }
        photos:
          type: array
          description: Optional, with `completed`. Links PropFlow fetches after the 202.
          items: { type: string, format: uri }
        labor_minutes: { type: integer, minimum: 0, description: 'Optional, with `completed`. Time on the job.' }
    WorkOrderRef:
      type: object
      description: The work order as the PMS knows it.
      required: [pms_account_id, pms_property_id, pms_work_order_id]
      properties:
        pms_account_id: { type: string, minLength: 1, description: 'The customer''s PMS account, e.g. their AppFolio database name.' }
        pms_property_id: { type: string, minLength: 1 }
        pms_work_order_id: { type: string, minLength: 1 }
    Accepted:
      type: object
      required: [id]
      properties:
        id: { type: string }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, description: What was wrong, in plain words. }
