openapi: 3.1.0
info:
  title: Fuuse Data API for the Be.EV HubSpot integration (proposed)
  version: 1.0.0-proposed
  summary: Delta-readable, warehouse-backed endpoints for sessions, drivers and chargers.
  description: |
    Proposed by Six & Flow on 8 September 2026 in response to Fuuse's offer to provide Data API endpoints
    that deliver the integration's data requirement in far fewer requests.

    Every collection accepts `updatedSince` and every record carries `lastModified`, so the integration reads
    only what changed. Every response carries `dataAsOf`, which the client stores and passes back as the next
    `updatedSince`. Sub-resources the integration would otherwise fetch per record (a driver's groups and
    promotions, a session's tariff and promotion) are embedded.

    Each schema property names the HubSpot property it feeds via `x-hubspot-property`, so it is clear that every
    requested field is in use. Fields marked `x-status: new` are not available from the operational API today.

    Companion narrative: `2026-09-08-fuuse-data-api-specification.md`.
  contact:
    name: Six & Flow
    email: lance@sixandflow.com
  license:
    name: Proprietary. Shared with Fuuse and Be.EV for the purpose of implementing the integration.
    identifier: LicenseRef-Proprietary

servers:
  - url: https://api.fuuse.io/data/v1
    description: Production (proposed base path)
  - url: https://staging-api.fuuse.io/data/v1
    description: Staging (proposed base path)

security:
  - bearerJwt: []

tags:
  - name: Sessions
    description: Priority 1. Replaces the per-charger session walk.
  - name: Drivers
    description: Priority 1. Replaces the five-minute driver poll, the per-driver detail read and the per-group promotion read.
  - name: Chargers
    description: Priority 2. Adam Plimley's daily charger dataset of 27 August with Natalie Kershaw's additions of 3 September.

paths:
  /sessions:
    get:
      tags: [Sessions]
      operationId: listSessions
      summary: Charging sessions across the estate, filtered by last-modified, status, start time and end time
      parameters:
        - $ref: '#/components/parameters/updatedSince'
        - $ref: '#/components/parameters/updatedUntil'
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/pageSize'
        - name: status
          in: query
          description: Default `completed`. The HubSpot deal is created once a session has ended. `inProgress` replaces the current re-read of open sessions.
          schema:
            type: string
            enum: [completed, inProgress, all]
            default: completed
        - name: startedSince
          in: query
          description: Inclusive lower bound on `startDateTime`, for bounded backfills by session date.
          schema:
            type: string
            format: date-time
        - name: startedUntil
          in: query
          description: Exclusive upper bound on `startDateTime`.
          schema:
            type: string
            format: date-time
        - name: endedSince
          in: query
          description: Inclusive lower bound on `endDateTime`. The deal is keyed on when a session ended, so this is the natural bound for backfilling a gap in completed sessions. Sessions with no `endDateTime` are excluded.
          schema:
            type: string
            format: date-time
        - name: endedUntil
          in: query
          description: Exclusive upper bound on `endDateTime`.
          schema:
            type: string
            format: date-time
        - name: driverId
          in: query
          schema:
            type: string
        - name: chargerId
          in: query
          schema:
            type: string
      responses:
        '200':
          description: A page of sessions.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PageMeta'
                  - type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Session'
              examples:
                hourlyDelta:
                  summary: Hourly delta read
                  value:
                    dataAsOf: '2026-09-08T14:00:00Z'
                    totalCount: 96
                    nextCursor: null
                    items:
                      - id: 9f3c1c2e-5b7c-4a1f-9a7e-2f0e2f1f1a11
                        status: completed
                        startDateTime: '2026-09-08T12:14:03Z'
                        endDateTime: '2026-09-08T13:02:47Z'
                        lastModified: '2026-09-08T13:03:10Z'
                        driverId: c1d2e3f4-0000-4000-8000-000000000001
                        charger:
                          chargerId: 12ab34cd
                          locationId: 56ef78gh
                          evseUid: GB*BEV*E12AB34CD*1
                          hardwareIdentifier: BEEV01
                        kwh: 21.4
                        co2Saving: 4.71
                        totalRevenue: 11.77
                        currency: GBP
                        tariff:
                          tariffId: t-001
                          tariffName: Member Standard
                        promotion:
                          promotionId: p-042
                          name: Your Be.EV Welcome Gift
                        sessionSource: app
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /drivers:
    get:
      tags: [Drivers]
      operationId: listDrivers
      summary: Drivers with groups and promotions embedded, filtered by last-modified
      description: |
        The primary call is a last-modified poll: `updatedSince=<previous dataAsOf>` returns only drivers whose
        `lastModified` moved since the last read, including changes to group membership, marketing consent,
        last login and lifetime totals. This replaces the current created-date sweep plus per-driver detail
        and per-group promotion requests. `createdSince` is for bounded backfills by sign-up date only.
      parameters:
        - $ref: '#/components/parameters/updatedSince'
        - $ref: '#/components/parameters/updatedUntil'
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/pageSize'
        - name: createdSince
          in: query
          description: Inclusive lower bound on `createdDate`, for bounded backfills.
          schema:
            type: string
            format: date-time
        - name: createdUntil
          in: query
          description: Exclusive upper bound on `createdDate`.
          schema:
            type: string
            format: date-time
        - name: groupId
          in: query
          schema:
            type: string
      responses:
        '200':
          description: A page of drivers.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PageMeta'
                  - type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Driver'
              examples:
                hourlyDelta:
                  summary: Hourly delta read
                  value:
                    dataAsOf: '2026-09-08T14:00:00Z'
                    totalCount: 41
                    nextCursor: null
                    items:
                      - id: c1d2e3f4-0000-4000-8000-000000000001
                        lastModified: '2026-09-08T13:41:22Z'
                        email: driver@example.com
                        firstName: Sam
                        lastName: Example
                        address1: 1 Example Street
                        town: Manchester
                        county: Greater Manchester
                        postcode: M1 1AA
                        country: United Kingdom
                        marketingConsent: true
                        marketingConsentDate: '2026-02-11T09:30:00Z'
                        createdDate: '2026-02-11T09:28:47Z'
                        lastLoginDate: '2026-09-08T13:41:22Z'
                        totalKwhConsumed: 412.6
                        co2Saving: 90.8
                        groups:
                          - groupId: g-017
                            name: Be.EV Members
                        promotions:
                          - promotionId: p-042
                            name: Your Be.EV Welcome Gift
                            groupId: g-017
                        deleted: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /drivers/{id}:
    get:
      tags: [Drivers]
      operationId: getDriver
      summary: One driver, same record shape as the list
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The driver.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Driver'
              examples:
                driver:
                  $ref: '#/components/examples/driver'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /chargers:
    get:
      tags: [Chargers]
      operationId: listChargers
      summary: Daily charger dataset, filtered by last-modified or by report date
      description: |
        One record per charger per `reportDate`. With `updatedSince`, return the latest report row for chargers
        whose data changed at or after that instant. With `reportDate`, return the full dataset for that day.
      parameters:
        - $ref: '#/components/parameters/updatedSince'
        - $ref: '#/components/parameters/updatedUntil'
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/pageSize'
        - name: reportDate
          in: query
          description: Return the full dataset for this report date.
          schema:
            type: string
            format: date
      responses:
        '200':
          description: A page of charger daily records.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PageMeta'
                  - type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/ChargerDaily'
              examples:
                dailyDelta:
                  summary: Daily delta read
                  value:
                    dataAsOf: '2026-09-08T00:00:00Z'
                    totalCount: 1
                    nextCursor: null
                    items:
                      - chargerId: 12ab34cd
                        hardwareIdentifier: BEEV01
                        locationId: 56ef78gh
                        manufacturer: Circontrol
                        model: R50
                        lastCommunicationReceived: '2026-09-07T18:42:15Z'
                        reportDate: '2026-09-07'
                        dataAsOf: '2026-09-08T00:00:00Z'
                        lastModified: '2026-09-08T00:05:12Z'
                        chargingSeconds: 15420
                        idleSeconds: 48600
                        downtimeSeconds: 22380
                        exemptSeconds: 0
                        uniqueDriverCount: 147
                        sessionCountTotal: 1893
                        site:
                          siteId: site-0042
                          siteName: Trafford Park Hub
                          locationName: Bay 3
                          town: Manchester
                          county: Greater Manchester
                          district: Trafford
                          region: North West
                          postcode: M17 1AB
                          latitude: 53.4668
                          longitude: -2.3167
                        powerRatingKw: 50
                        connectorCount: 2
                        commissionedDate: '2024-03-18'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

webhooks:
  sessionCompleted:
    post:
      operationId: webhookSessionCompleted
      summary: Phase 2. Fired when a session ends, carrying the session record.
      description: Signed with HMAC-SHA256 over the raw body using a shared secret, sent as `X-Fuuse-Signature`. Retried on non-2xx.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event, occurredAt, data]
              properties:
                event:
                  type: string
                  const: session.completed
                occurredAt:
                  type: string
                  format: date-time
                data:
                  $ref: '#/components/schemas/Session'
      responses:
        '200':
          description: Acknowledged.
  driverUpdated:
    post:
      operationId: webhookDriverUpdated
      summary: Phase 2. Fired when a driver record changes, carrying the driver record.
      description: Same signing and retry semantics as `sessionCompleted`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event, occurredAt, data]
              properties:
                event:
                  type: string
                  const: driver.updated
                occurredAt:
                  type: string
                  format: date-time
                data:
                  $ref: '#/components/schemas/Driver'
      responses:
        '200':
          description: Acknowledged.

components:
  securitySchemes:
    bearerJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT obtained from the existing `POST /token/` endpoint with `clientKey` and `clientSecret`.

  parameters:
    updatedSince:
      name: updatedSince
      in: query
      description: Inclusive lower bound on `lastModified`. Omit for a full snapshot. The client passes back the previous response's `dataAsOf`.
      schema:
        type: string
        format: date-time
      example: '2026-09-08T13:00:00Z'
    updatedUntil:
      name: updatedUntil
      in: query
      description: Exclusive upper bound on `lastModified`, for bounded backfills.
      schema:
        type: string
        format: date-time
    cursor:
      name: cursor
      in: query
      description: Opaque cursor from the previous page's `nextCursor`.
      schema:
        type: string
    pageSize:
      name: pageSize
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 2000
        default: 500

  headers:
    X-RateLimit-Limit:
      description: Requests allowed per window. Present on every response.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Seconds until the window resets.
      schema:
        type: integer
    Link:
      description: RFC 8288 link header with `rel="next"` when another page exists.
      schema:
        type: string
      example: '<https://api.fuuse.io/data/v1/sessions?cursor=eyJvZmZzZXQiOjUwMH0>; rel="next"'

  responses:
    BadRequest:
      description: Invalid parameter.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or expired token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: No such record.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Rate limit exceeded. Always JSON, never an HTML challenge page, with `Retry-After` in seconds.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            status: 429
            code: rate_limited
            message: Retry after 12 seconds
            retryAfterSeconds: 12

  schemas:
    PageMeta:
      type: object
      required: [dataAsOf, totalCount]
      properties:
        dataAsOf:
          type: string
          format: date-time
          description: Warehouse high-water mark up to which this result is complete. The client stores it and sends it back as the next `updatedSince`.
        totalCount:
          type: integer
          description: Total records matching the query across all pages.
        nextCursor:
          type: [string, 'null']
          description: Cursor for the next page. Null or absent on the last page.

    Error:
      type: object
      required: [status, code, message]
      properties:
        status:
          type: integer
        code:
          type: string
        message:
          type: string
        retryAfterSeconds:
          type: integer

    ChargerRef:
      type: object
      required: [chargerId, locationId, evseUid]
      properties:
        chargerId:
          type: string
          description: Stable charger key, matching `chargerId` on the chargers endpoint.
          x-status: new
        locationId:
          type: string
          x-hubspot-property: dealname (component)
        evseUid:
          type: string
          x-hubspot-property: dealname (component)
        hardwareIdentifier:
          type: string
          x-hubspot-property: chargers.hardware_identifier (match key)

    Tariff:
      type: object
      required: [tariffId]
      properties:
        tariffId:
          type: string
        tariffName:
          type: string
          x-hubspot-property: deal.tariff

    PromotionRef:
      type: object
      required: [promotionId, name]
      properties:
        promotionId:
          type: string
          x-hubspot-property: deal.promotion_id
        name:
          type: string
          x-hubspot-property: deal.promotion_name

    Session:
      type: object
      required: [id, status, startDateTime, lastModified, driverId, charger, kwh, co2Saving, totalRevenue]
      properties:
        id:
          type: string
          x-hubspot-property: deal.fuuse_session_id
        status:
          type: string
          enum: [completed, inProgress]
        startDateTime:
          type: string
          format: date-time
          x-hubspot-property: deal.start_time
        endDateTime:
          type: [string, 'null']
          format: date-time
          description: Null while in progress. Its presence is what allows the deal to be created.
          x-hubspot-property: deal.end_time
        lastModified:
          type: string
          format: date-time
          description: Moves on any change, including post-completion corrections to kWh or revenue.
        driverId:
          type: string
          description: Resolves the association to the HubSpot contact via `fuuse_driver_id`.
        charger:
          $ref: '#/components/schemas/ChargerRef'
        kwh:
          type: number
          x-hubspot-property: deal.kwh
        co2Saving:
          type: number
          x-hubspot-property: deal.co2_saving
        totalRevenue:
          type: number
          x-hubspot-property: deal.amount
        currency:
          type: string
          description: ISO 4217 code for `totalRevenue`.
          x-status: new
        tariff:
          $ref: '#/components/schemas/Tariff'
        promotion:
          $ref: '#/components/schemas/PromotionRef'
        sessionSource:
          type: string
          x-hubspot-property: deal.session_source

    DriverGroup:
      type: object
      required: [groupId, name]
      properties:
        groupId:
          type: string
        name:
          type: string
          x-hubspot-property: contact.fuuse_driver_group (joined with ';')

    DriverPromotion:
      type: object
      required: [promotionId, name]
      properties:
        promotionId:
          type: string
        name:
          type: string
          x-hubspot-property: contact.fuuse_driver_promotion (joined with ';')
        groupId:
          type: string
          description: The group through which the driver holds this promotion.

    Driver:
      type: object
      required: [id, lastModified, email, marketingConsent, createdDate, groups, promotions]
      properties:
        id:
          type: string
          x-hubspot-property: contact.fuuse_driver_id
        lastModified:
          type: string
          format: date-time
          description: Moves on any change to this record, including group membership, consent and last login.
        email:
          type: string
          format: email
          x-hubspot-property: contact.email
        firstName:
          type: string
          x-hubspot-property: contact.firstname
        lastName:
          type: string
          x-hubspot-property: contact.lastname
        address1:
          type: string
          x-hubspot-property: contact.address
        address2:
          type: string
          x-hubspot-property: contact.address2
        town:
          type: string
          x-hubspot-property: contact.city
        county:
          type: string
          x-hubspot-property: contact.county
        postcode:
          type: string
          x-hubspot-property: contact.zip
        country:
          type: string
          x-hubspot-property: contact.country
        marketingConsent:
          type: boolean
          x-hubspot-property: contact.fuuse_marketing_consent
        marketingConsentDate:
          type: [string, 'null']
          format: date-time
          x-hubspot-property: contact.fuuse_marketing_consent_date
        createdDate:
          type: string
          format: date-time
          x-hubspot-property: contact.fuuse_create_date
        lastLoginDate:
          type: [string, 'null']
          format: date-time
          x-hubspot-property: contact.fuuse_last_login_date
        totalKwhConsumed:
          type: number
          x-hubspot-property: contact.total_kwh_consumed
        co2Saving:
          type: number
          x-hubspot-property: contact.co2_saving
        groups:
          type: array
          items:
            $ref: '#/components/schemas/DriverGroup'
        promotions:
          type: array
          description: Promotions granted through the driver's groups. Embedding this removes one request per group per driver.
          items:
            $ref: '#/components/schemas/DriverPromotion'
        deleted:
          type: boolean
          default: false
          description: True when the driver has been removed, so deletions can be handled without a full snapshot diff.
          x-status: new

    Site:
      type: object
      description: Location fields Be.EV currently load into HubSpot by hand each month. Lower priority per Natalie Kershaw, 3 September.
      properties:
        siteId:
          type: string
          x-hubspot-property: chargers.site_id
        siteName:
          type: string
          x-hubspot-property: chargers.site_name
        locationName:
          type: string
          x-hubspot-property: chargers.location_name
        town:
          type: string
          x-hubspot-property: chargers.town
        county:
          type: string
          x-hubspot-property: chargers.county
        district:
          type: string
          x-hubspot-property: chargers.district
        region:
          type: string
          x-hubspot-property: chargers.region
        postcode:
          type: string
          x-hubspot-property: chargers.zip
        latitude:
          type: number
        longitude:
          type: number

    ChargerDaily:
      type: object
      description: Adam Plimley's daily charger dataset (27 August) plus the additions retained by Natalie Kershaw (3 September).
      required: [chargerId, hardwareIdentifier, locationId, reportDate, dataAsOf, lastModified, chargingSeconds, idleSeconds, downtimeSeconds, exemptSeconds, uniqueDriverCount]
      properties:
        chargerId:
          type: string
        hardwareIdentifier:
          type: string
          x-hubspot-property: chargers.hardware_identifier
        locationId:
          type: string
          x-hubspot-property: chargers.location_id
        manufacturer:
          type: string
          x-hubspot-property: chargers.manufacturer
        model:
          type: string
          x-hubspot-property: chargers.model
        lastCommunicationReceived:
          type: string
          format: date-time
          x-hubspot-property: chargers.last_used_date
        reportDate:
          type: string
          format: date
        dataAsOf:
          type: string
          format: date-time
        lastModified:
          type: string
          format: date-time
          x-status: new
        chargingSeconds:
          type: integer
          description: Durations rather than percentages, as Adam proposed. We compute the percentage over the window we need.
          x-hubspot-property: chargers.charge_time (derived)
        idleSeconds:
          type: integer
          x-hubspot-property: chargers.idle_time (derived)
        downtimeSeconds:
          type: integer
          x-hubspot-property: chargers.down_time (derived)
        exemptSeconds:
          type: integer
          description: Excluded from the percentage denominator.
        uniqueDriverCount:
          type: integer
          description: Total unique drivers to date, as stored today (Marcin, 3 September). Whether to scope to members only is an open question from Natalie.
          x-hubspot-property: chargers.unique_drivers
        sessionCountTotal:
          type: integer
          description: All-time session count, as stored today.
          x-hubspot-property: chargers.sessions
        site:
          $ref: '#/components/schemas/Site'
        powerRatingKw:
          type: number
          x-hubspot-property: chargers.fast_rapid_ultra_rapid (band derived)
        connectorCount:
          type: integer
        commissionedDate:
          type: string
          format: date

  examples:
    driver:
      summary: A driver with one group and one promotion
      value:
        id: c1d2e3f4-0000-4000-8000-000000000001
        lastModified: '2026-09-08T13:41:22Z'
        email: driver@example.com
        firstName: Sam
        lastName: Example
        address1: 1 Example Street
        town: Manchester
        county: Greater Manchester
        postcode: M1 1AA
        country: United Kingdom
        marketingConsent: true
        marketingConsentDate: '2026-02-11T09:30:00Z'
        createdDate: '2026-02-11T09:28:47Z'
        lastLoginDate: '2026-09-08T13:41:22Z'
        totalKwhConsumed: 412.6
        co2Saving: 90.8
        groups:
          - groupId: g-017
            name: Be.EV Members
        promotions:
          - promotionId: p-042
            name: Your Be.EV Welcome Gift
            groupId: g-017
        deleted: false
