asyncapi: 2.6.0

info:
  title: DirtFleet webhooks
  version: "1.0.0"
  description: |
    AsyncAPI specification for DirtFleet's outbound webhook event
    surface. Complements the REST API spec at /openapi.yaml — that one
    documents request/response; this one documents the asynchronous
    event stream the customer's receiver subscribes to.

    Delivery transport: HTTPS POST. Signature: HMAC-SHA256 over
    `${timestamp}.${rawBody}` with the subscription secret. Headers:
    `X-Fleetgo-Signature`, `X-Fleetgo-Timestamp`, `X-Fleetgo-Event`.
    Replay window: 5 minutes (300 seconds).

    Subscription management is in the REST API: `/api/v1/webhooks`
    (list / create / delete) and `/api/v1/webhooks/{id}` (toggle
    active, update URL / events). Test delivery: POST
    `/api/v1/webhooks/{id}/test`.
  contact:
    name: DirtFleet support
    email: support@dirtfleet.app
    url: https://dirtfleet.app/support
  license:
    name: Proprietary

servers:
  receiver:
    url: '{receiverUrl}'
    protocol: https
    description: |
      The customer-supplied HTTPS endpoint registered as a
      WebhookSubscription URL. DirtFleet POSTs to this URL when
      matching events fire.
    variables:
      receiverUrl:
        default: https://your-app.example.com/webhooks/dirtfleet
        description: The URL configured on the subscription.

defaultContentType: application/json

channels:
  flag.created:
    description: |
      A YELLOW or RED flag was raised against an asset — by a driver
      from the app, or auto-spawned by anomaly detection.
    subscribe:
      operationId: receiveFlagCreated
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/FlagCreated'

  flag.resolved:
    description: A flag was resolved (status flipped to RESOLVED).
    subscribe:
      operationId: receiveFlagResolved
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/FlagResolved'

  hours.logged:
    description: A new HoursLog row was written for an asset.
    subscribe:
      operationId: receiveHoursLogged
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/HoursLogged'

  asset.created:
    description: A new Asset row was created in the org.
    subscribe:
      operationId: receiveAssetCreated
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/AssetCreated'

  asset.updated:
    description: |
      An asset's identity / plates / renewals / financials /
      lifecycle changed.
    subscribe:
      operationId: receiveAssetUpdated
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/AssetUpdated'

  workorder.created:
    description: A new WorkOrder was created.
    subscribe:
      operationId: receiveWorkOrderCreated
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/WorkOrderCreated'

  workorder.completed:
    description: |
      A WorkOrder flipped to DONE (manually or via a RepairLog close).
    subscribe:
      operationId: receiveWorkOrderCompleted
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/WorkOrderCompleted'

  tool.failure:
    description: A Tool was reported BROKEN.
    subscribe:
      operationId: receiveToolFailure
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolFailure'

  tool.checked_out:
    description: A Tool was checked out to a user / vehicle.
    subscribe:
      operationId: receiveToolCheckedOut
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolCheckedOut'

  tool.checked_in:
    description: |
      A Tool was checked back in. Records the observed condition; a
      `BROKEN` check-in also triggers `tool.failure` for subscribers
      that only care about the failure side.
    subscribe:
      operationId: receiveToolCheckedIn
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolCheckedIn'

  tool.low_stock:
    description: |
      A consumable Tool's stockLevel just crossed from above-threshold
      to at-or-below-threshold via an adjust-stock call. Only the
      crossing fires the event — going further below threshold or
      restocking back above does not retrigger it. Wire this to
      reorder automation, Slack, or a daily-summary email.
    subscribe:
      operationId: receiveToolLowStock
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolLowStock'

  tool.assignment_changed:
    description: |
      A Tool's assigned owner moved: assignedUserId, assignedVehicleId,
      assignedYardId, or parentKitId changed via PATCH /tools/{id}.
      A single PATCH that touches multiple assigned* fields emits one
      delivery whose `changes` array lists every kind that moved.
      Mirrors the internal ASSIGNMENT ToolEvent audit feed — wire this
      to accounting / HR systems that maintain a parallel ownership
      ledger and don't want to poll the events endpoint.
    subscribe:
      operationId: receiveToolAssignmentChanged
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolAssignmentChanged'

  tool.serviced:
    description: |
      A PM service was recorded on a tool via POST
      /tools/{id}/mark-serviced. Carries the actor, the new
      lastServicedAt, the prior lastServicedAt (null on first service),
      the PM interval in days, and any free-form note. Mirrors the
      internal `pm_serviced:` AUDIT ToolEvent — wire this to asset
      depreciation systems, OEM warranty trackers, and CMMS exports
      that maintain a parallel service-history ledger.
    subscribe:
      operationId: receiveToolServiced
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolServiced'

  tool.created:
    description: |
      A new Tool was created via POST /tools (single) or POST
      /tools/batch (bulk import). Fires once per row — a 50-row
      bulk import emits 50 deliveries. Mirror of asset.created;
      subscribe this to keep an accounting / asset-register ledger
      in lockstep with DirtFleet without nightly reconciliation.
    subscribe:
      operationId: receiveToolCreated
      bindings:
        http:
          type: request
          method: POST
      message:
        $ref: '#/components/messages/ToolCreated'

components:
  messageTraits:
    SignedWebhook:
      headers:
        type: object
        required: [x-fleetgo-signature, x-fleetgo-timestamp, x-fleetgo-event]
        properties:
          x-fleetgo-signature:
            type: string
            description: |
              `v1=` + lowercase hex HMAC-SHA256 of
              `${timestamp}.${rawBody}` using the subscription secret.
            example: v1=8c7e1b4a3d6a4a2f9f5c2e3d8c7e1b4a3d6a4a2f9f5c2e3d8c7e1b4a3d6a4a2f
          x-fleetgo-timestamp:
            type: string
            description: Unix epoch seconds at signing time.
            example: "1747260000"
          x-fleetgo-event:
            type: string
            description: |
              The event type, mirrors the channel name. Lets receivers
              route without parsing the body first.
            example: flag.created
          x-fleetgo-test:
            type: string
            description: |
              Present only on synthetic test deliveries (from
              `/api/v1/webhooks/{id}/test`). Production traffic never
              has this header.
            enum: ["1"]
      bindings:
        http:
          type: request

  messages:
    FlagCreated:
      name: FlagCreated
      title: flag.created
      summary: A new YELLOW or RED flag was raised against an asset.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [flag.created]
              data:
                type: object
                required: [flagId, assetId, severity]
                properties:
                  flagId: { type: string }
                  assetId: { type: string }
                  assetNickname: { type: string }
                  severity:
                    type: string
                    enum: [YELLOW, RED]
                  reason:
                    type: string
                    description: Note from the driver, if any.
                  photoUrl:
                    type: [string, "null"]
                    format: uri
                  raisedBy:
                    type: object
                    properties:
                      userId: { type: [string, "null"] }
                      name: { type: [string, "null"] }
                  raisedAt:
                    type: string
                    format: date-time

    FlagResolved:
      name: FlagResolved
      title: flag.resolved
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [flag.resolved]
              data:
                type: object
                required: [flagId, assetId]
                properties:
                  flagId: { type: string }
                  assetId: { type: string }
                  resolvedBy:
                    type: object
                    properties:
                      userId: { type: [string, "null"] }
                      name: { type: [string, "null"] }
                  resolutionNote: { type: [string, "null"] }
                  resolvedAt: { type: string, format: date-time }

    HoursLogged:
      name: HoursLogged
      title: hours.logged
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [hours.logged]
              data:
                type: object
                required: [logId, assetId]
                properties:
                  logId: { type: string }
                  assetId: { type: string }
                  hoursReading: { type: [number, "null"] }
                  delta: { type: [number, "null"] }
                  state:
                    type: [string, "null"]
                    enum: [IN_USE, IDLE, OUT_OF_SERVICE, null]
                  loggedAt: { type: string, format: date-time }

    AssetCreated:
      name: AssetCreated
      title: asset.created
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [asset.created]
              data:
                type: object
                required: [assetId, nickname, assetClass]
                properties:
                  assetId: { type: string }
                  nickname: { type: string }
                  assetClass:
                    type: string
                    enum: [on-road, off-road, trailer]
                  meterType:
                    type: string
                    enum: [HOURS, ODOMETER, NONE]

    AssetUpdated:
      name: AssetUpdated
      title: asset.updated
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [asset.updated]
              data:
                type: object
                required: [assetId]
                properties:
                  assetId: { type: string }
                  changedFields:
                    type: array
                    items: { type: string }
                    description: |
                      Names of fields whose value changed in this
                      update. Useful for routing — receivers can skip
                      events that didn't touch the fields they care
                      about.

    WorkOrderCreated:
      name: WorkOrderCreated
      title: workorder.created
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [workorder.created]
              data:
                type: object
                required: [workOrderId, number, title, status]
                properties:
                  workOrderId: { type: string }
                  number: { type: integer }
                  title: { type: string }
                  status:
                    type: string
                    enum: [OPEN, ASSIGNED, IN_PROGRESS]
                  priority:
                    type: string
                    enum: [LOW, NORMAL, HIGH, URGENT]
                  assetId: { type: [string, "null"] }
                  flagId: { type: [string, "null"] }
                  assignedToId: { type: [string, "null"] }

    WorkOrderCompleted:
      name: WorkOrderCompleted
      title: workorder.completed
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [workorder.completed]
              data:
                type: object
                required: [workOrderId, number, status]
                properties:
                  workOrderId: { type: string }
                  number: { type: integer }
                  status:
                    type: string
                    enum: [DONE]
                  completedAt: { type: string, format: date-time }
                  repairId:
                    type: [string, "null"]
                    description: Set when completion was via a RepairLog.

    ToolFailure:
      name: ToolFailure
      title: tool.failure
      summary: A tool was reported BROKEN.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.failure]
              data:
                type: object
                required: [toolId, name]
                properties:
                  toolId: { type: string }
                  name: { type: string }
                  category: { type: [string, "null"] }
                  reportedBy:
                    type: object
                    properties:
                      userId: { type: [string, "null"] }
                      name: { type: [string, "null"] }
                  notes: { type: [string, "null"] }

    ToolCheckedOut:
      name: ToolCheckedOut
      title: tool.checked_out
      summary: A tool was checked out to a user or vehicle.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.checked_out]
              data:
                type: object
                required: [toolId, eventId, actorId, toUserId, occurredAt]
                properties:
                  toolId: { type: string }
                  eventId:
                    type: string
                    description: The new ToolEvent.id (CHECK_OUT row).
                  actorId:
                    type: string
                    description: User who recorded the checkout (key creator on API path).
                  toUserId:
                    type: string
                    description: User the tool was checked out to (defaults to actorId).
                  toVehicleId: { type: [string, "null"] }
                  linkedAssetId: { type: [string, "null"] }
                  occurredAt: { type: string, format: date-time }

    ToolCheckedIn:
      name: ToolCheckedIn
      title: tool.checked_in
      summary: A tool was checked back in (with observed condition).
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.checked_in]
              data:
                type: object
                required:
                  - toolId
                  - eventId
                  - actorId
                  - condition
                  - newStatus
                  - occurredAt
                properties:
                  toolId: { type: string }
                  eventId: { type: string }
                  actorId: { type: string }
                  condition:
                    type: string
                    enum: [GOOD, NEEDS_ATTENTION, BROKEN]
                  newStatus:
                    type: string
                    enum: [GOOD, NEEDS_ATTENTION, BROKEN]
                    description: Resulting Tool.status after the check-in.
                  workOrderId:
                    type: [string, "null"]
                    description: |
                      Non-null only when `condition: BROKEN` — the
                      auto-spawned WorkOrder id. A BROKEN check-in
                      also fires a separate `tool.failure` event for
                      subscribers that only watch failures.
                  occurredAt: { type: string, format: date-time }

    ToolLowStock:
      name: ToolLowStock
      title: tool.low_stock
      summary: |
        A consumable Tool just crossed from above-threshold to
        at-or-below-threshold via a stock adjustment. Fires once on
        the crossing — subsequent decrements don't retrigger.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.low_stock]
              data:
                type: object
                required:
                  - toolId
                  - eventId
                  - actorId
                  - previousStockLevel
                  - newStockLevel
                  - thresholdUsed
                  - occurredAt
                properties:
                  toolId: { type: string }
                  eventId:
                    type: string
                    description: The AUDIT ToolEvent.id that recorded the adjustment.
                  actorId: { type: string }
                  previousStockLevel: { type: integer }
                  newStockLevel: { type: integer }
                  minStockLevel:
                    type: [integer, "null"]
                    description: |
                      The tool's per-row override, or null when using
                      the org-wide fallback. Use `thresholdUsed` for
                      the effective comparison.
                  thresholdUsed:
                    type: integer
                    description: |
                      The threshold value the engine actually compared
                      against — `minStockLevel` if set, else 5.
                  occurredAt: { type: string, format: date-time }

    ToolAssignmentChanged:
      name: ToolAssignmentChanged
      title: tool.assignment_changed
      summary: |
        A Tool's ownership moved (user / vehicle / yard / kit). Mirrors
        the internal ASSIGNMENT ToolEvent audit feed; a single PATCH
        emits one delivery whose `changes` array lists every kind that
        moved in that update.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.assignment_changed]
              data:
                type: object
                required: [toolId, actorId, changes, at]
                properties:
                  toolId: { type: string }
                  actorId:
                    type: [string, "null"]
                    description: |
                      The user who initiated the PATCH. Null on system
                      paths (e.g. CSV bulk import) where no user is
                      attributed.
                  changes:
                    type: array
                    minItems: 1
                    items:
                      type: object
                      required: [kind, from, to]
                      properties:
                        kind:
                          type: string
                          enum: [user, vehicle, yard, kit]
                        from:
                          type: [string, "null"]
                          description: Previous id (null = unassigned before).
                        to:
                          type: [string, "null"]
                          description: New id (null = unassigned after).
                  at: { type: string, format: date-time }

    ToolServiced:
      name: ToolServiced
      title: tool.serviced
      summary: |
        A PM service was recorded on a tool. Mirrors the internal
        `pm_serviced:` AUDIT ToolEvent.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.serviced]
              data:
                type: object
                required:
                  - toolId
                  - eventId
                  - actorId
                  - pmIntervalDays
                  - previousLastServicedAt
                  - newLastServicedAt
                properties:
                  toolId: { type: string }
                  eventId:
                    type: string
                    description: The AUDIT ToolEvent.id recording the service.
                  actorId: { type: string }
                  pmIntervalDays:
                    type: integer
                    description: |
                      The tool's PM cadence at the time of service.
                      Mirroring systems use this to compute the next
                      due date without a second lookup.
                  previousLastServicedAt:
                    type: [string, "null"]
                    format: date-time
                    description: |
                      Null when the tool was being serviced for the
                      first time.
                  newLastServicedAt:
                    type: string
                    format: date-time
                  note:
                    type: [string, "null"]
                    description: |
                      Free-form note attached to the service, ≤500
                      chars. Null when no note was provided.

    ToolCreated:
      name: ToolCreated
      title: tool.created
      summary: |
        A new Tool was created. Bulk-import paths emit one delivery
        per row.
      traits:
        - $ref: '#/components/messageTraits/SignedWebhook'
      payload:
        allOf:
          - $ref: '#/components/schemas/Envelope'
          - type: object
            properties:
              event:
                type: string
                enum: [tool.created]
              data:
                type: object
                required:
                  - toolId
                  - name
                  - isConsumable
                  - scanToken
                  - actorId
                  - at
                properties:
                  toolId: { type: string }
                  name: { type: string }
                  category:
                    type: [string, "null"]
                    description: Free-form category tag (e.g. "wrench").
                  isConsumable: { type: boolean }
                  scanToken:
                    type: string
                    description: |
                      The opaque QR / NFC token that resolves back to
                      this Tool. Receivers that label inventory at
                      creation can render the QR straight from this.
                  purchaseCost:
                    type: [number, "null"]
                    description: |
                      USD purchase cost when provided at creation;
                      null otherwise. Useful for accounting systems
                      that prime the depreciation schedule from this
                      event.
                  actorId:
                    type: [string, "null"]
                    description: |
                      The user who created the tool. Null on bulk
                      import or system paths that don't attribute to
                      a user.
                  at: { type: string, format: date-time }

  schemas:
    Envelope:
      description: |
        Every webhook payload starts with these five fields. The `id`
        doubles as the recommended idempotency key — receivers
        should persist it to a unique-indexed table inside the same
        transaction as their side effect, so retries (which WILL
        happen on 5xx + network errors) don't double-fire.
      type: object
      required: [id, event, organizationId, sentAt, data]
      properties:
        id:
          type: string
          description: |
            Stable delivery id. Use as the idempotency key when
            persisting side effects.
        event:
          type: string
          enum:
            - flag.created
            - flag.resolved
            - hours.logged
            - asset.created
            - asset.updated
            - workorder.created
            - workorder.completed
            - tool.failure
            - tool.checked_out
            - tool.checked_in
            - tool.low_stock
            - tool.assignment_changed
            - tool.serviced
            - tool.created
        organizationId:
          type: string
          description: The DirtFleet org this event belongs to.
        sentAt:
          type: string
          format: date-time
        data:
          type: object
          description: Event-specific payload — see each message definition.
