openapi: 3.1.0
info:
  title: Ecomilhas Platform API
  version: 1.0.0-draft
  summary: Sustainable mobility rewards, trip capture, Mastercard wallet and CO2e inventory
  description: |
    Draft specification for the Ecomilhas integration platform.

    **Status: in planning — no endpoint is available in production yet.**

    Modules:
    - **Balance & Wallet** — user balance, transactions, credits, Mastercard push provisioning
    - **Trips** — automatic trip capture with ecomiles and avoided-CO2e calculation
    - **Impact & Inventory** — consolidated organizational impact, GHG Protocol inventory, ESG export
    - **Companies** — campaigns, bulk credits, member management
    - **Widget** — embeddable white-label widget sessions
    - **Webhooks** — event notifications (documented under `webhooks`)
  contact:
    name: Ecomilhas Developers
    email: developers@ecomiles.app
    url: https://ecomilhas.notion.site/Ecomilhas-API-3904b6dc237e8050ba61d4363e59c987
servers:
  - url: https://api.ecomilhas.com/v1
    description: Production (planned)
  - url: https://sandbox.api.ecomilhas.com/v1
    description: Sandbox (planned)

security:
  - apiKey: []
  - oauthClientCredentials: []

tags:
  - name: Balance & Wallet
  - name: Trips
  - name: Impact
  - name: Inventories
  - name: Companies
  - name: Widget

paths:
  /users/{user_id}/balance:
    get:
      tags: [Balance & Wallet]
      operationId: getUserBalance
      summary: Get a user's ecomiles balance
      security:
        - oauthAuthCode: [balance:read]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Current balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Balance'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /users/{user_id}/transactions:
    get:
      tags: [Balance & Wallet]
      operationId: listUserTransactions
      summary: List a user's ecomiles transactions
      security:
        - oauthAuthCode: [balance:read]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/UserId'
        - name: type
          in: query
          schema:
            type: string
            enum: [EARN, CREDIT, REDEEM, EXPIRE, ADJUST]
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Paginated transaction list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Transaction' }
                  next_cursor:
                    type: [string, 'null']
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /users/{user_id}/credits:
    post:
      tags: [Balance & Wallet]
      operationId: creditUser
      summary: Credit ecomiles to a user (partner-issued)
      parameters:
        - $ref: '#/components/parameters/UserId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreditRequest'
      responses:
        '201':
          description: Credit applied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transaction'
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /wallet/provisioning-token:
    post:
      tags: [Balance & Wallet]
      operationId: createProvisioningToken
      summary: Create a Mastercard push-provisioning token (Apple Pay / Google Wallet)
      description: Returns an opaque payment card payload (MDES) to hand to the device wallet SDK. The PAN is never exposed.
      security:
        - oauthAuthCode: [wallet:provision]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, wallet, device_id]
              properties:
                user_id: { type: string }
                wallet:
                  type: string
                  enum: [APPLE_PAY, GOOGLE_PAY]
                device_id: { type: string }
      responses:
        '201':
          description: Short-lived provisioning payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  opaque_payment_card: { type: string }
                  expires_in: { type: integer, example: 300 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /trips:
    post:
      tags: [Trips]
      operationId: createTrip
      summary: Register a single trip
      security:
        - oauthAuthCode: [trips:write]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TripRequest'
      responses:
        '201':
          description: Trip processed (or accepted for async processing)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TripResult'
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /trips:bulk:
    post:
      tags: [Trips]
      operationId: createTripsBulk
      summary: Register trips in batch (up to 500)
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [trips]
              properties:
                trips:
                  type: array
                  maxItems: 500
                  items: { $ref: '#/components/schemas/TripRequest' }
      responses:
        '202':
          description: Batch accepted for processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch_id: { type: string, example: btc_1a2b3c }
                  accepted: { type: integer }
                  rejected: { type: integer }
        '400': { $ref: '#/components/responses/BadRequest' }

  /trips/{trip_id}:
    get:
      tags: [Trips]
      operationId: getTrip
      summary: Get a trip and its calculation result
      parameters:
        - name: trip_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Trip detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TripResult'
        '404': { $ref: '#/components/responses/NotFound' }

  /organizations/{org_id}/impact:
    get:
      tags: [Impact]
      operationId: getOrganizationImpact
      summary: Consolidated impact for an organization
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - name: period_start
          in: query
          required: true
          schema: { type: string, format: date }
        - name: period_end
          in: query
          required: true
          schema: { type: string, format: date }
        - name: group_by
          in: query
          schema:
            type: string
            enum: [modal, month, unit]
      responses:
        '200':
          description: Consolidated impact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImpactReport'
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /organizations/{org_id}/impact/export:
    get:
      tags: [Impact]
      operationId: exportOrganizationImpact
      summary: Export impact data for ESG platforms (GHG Protocol structure)
      description: Structured export by scope/category/period, ready for WayCarbon, Clima and inventory spreadsheets.
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - name: period_start
          in: query
          required: true
          schema: { type: string, format: date }
        - name: period_end
          in: query
          required: true
          schema: { type: string, format: date }
        - name: format
          in: query
          schema:
            type: string
            enum: [ghg, csv, json]
            default: ghg
      responses:
        '200':
          description: Export payload (JSON) or file (CSV)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GhgExport'
            text/csv:
              schema: { type: string }
        '403': { $ref: '#/components/responses/Forbidden' }

  /organizations/{org_id}/members:
    get:
      tags: [Companies]
      operationId: listMembers
      summary: List members linked to the organization (employees, couriers, fans)
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Paginated member list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Member' }
                  next_cursor:
                    type: [string, 'null']
    post:
      tags: [Companies]
      operationId: addMembers
      summary: Link members to the organization
      parameters:
        - $ref: '#/components/parameters/OrgId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [members]
              properties:
                members:
                  type: array
                  items:
                    type: object
                    required: [external_member_id]
                    properties:
                      external_member_id: { type: string }
                      email: { type: string, format: email }
                      role:
                        type: string
                        enum: [EMPLOYEE, COURIER, FAN, DRIVER]
      responses:
        '201':
          description: Members linked
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: integer }
                  rejected: { type: integer }

  /companies/{org_id}/campaigns:
    post:
      tags: [Companies]
      operationId: createCampaign
      summary: Create a campaign (event multipliers, goals)
      parameters:
        - $ref: '#/components/parameters/OrgId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignRequest'
      responses:
        '201':
          description: Campaign created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
        '422': { $ref: '#/components/responses/Unprocessable' }

  /companies/{org_id}/bulk-credits:
    post:
      tags: [Companies]
      operationId: bulkCredits
      summary: Distribute ecomiles credits in batch
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [credits]
              properties:
                credits:
                  type: array
                  maxItems: 1000
                  items:
                    type: object
                    required: [user_id, amount]
                    properties:
                      user_id: { type: string }
                      amount: { type: number, exclusiveMinimum: 0 }
                      reason: { type: string }
                      external_ref: { type: string }
      responses:
        '202':
          description: Batch accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch_id: { type: string }
                  accepted: { type: integer }
                  rejected: { type: integer }

  /widget/sessions:
    post:
      tags: [Widget]
      operationId: createWidgetSession
      summary: Create a short-lived session to embed the Ecomilhas widget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id]
              properties:
                user_id: { type: string }
                theme:
                  type: object
                  properties:
                    primary_color: { type: string, example: '#CB3524' }
                    logo_url: { type: string, format: uri }
                    dark_mode: { type: boolean }
                modules:
                  type: array
                  items:
                    type: string
                    enum: [balance, trips, redeem, leaderboard, impact]
                locale:
                  type: string
                  enum: [pt-BR, en, es]
                  default: pt-BR
      responses:
        '201':
          description: Widget session
          content:
            application/json:
              schema:
                type: object
                properties:
                  widget_url: { type: string, format: uri }
                  token: { type: string }
                  expires_in: { type: integer, example: 900 }

  /inventories:
    post:
      tags: [Inventories]
      operationId: createInventory
      summary: Create an emissions inventory (period/batch)
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryRequest'
      responses:
        '201':
          description: Inventory created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inventory'
        '409': { $ref: '#/components/responses/Conflict' }

  /inventories/{inventory_id}:
    get:
      tags: [Inventories]
      operationId: getInventory
      summary: Get inventory processing status
      parameters:
        - $ref: '#/components/parameters/InventoryId'
      responses:
        '200':
          description: Inventory status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryStatus'
        '404': { $ref: '#/components/responses/NotFound' }

  /inventories/{inventory_id}/items:bulk:
    post:
      tags: [Inventories]
      operationId: addInventoryItems
      summary: Send activity-data items in batch
      parameters:
        - $ref: '#/components/parameters/InventoryId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  items: { $ref: '#/components/schemas/InventoryItem' }
      responses:
        '202':
          description: Items accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  inventory_id: { type: string }
                  accepted: { type: integer }
                  rejected: { type: integer }
                  status: { type: string, example: RECEIVED }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /inventories/{inventory_id}/results:
    get:
      tags: [Inventories]
      operationId: getInventoryResults
      summary: Get consolidated inventory results
      parameters:
        - $ref: '#/components/parameters/InventoryId'
      responses:
        '200':
          description: Consolidated results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryResults'
        '404': { $ref: '#/components/responses/NotFound' }

webhooks:
  trip.processed:
    post:
      summary: Trip validated and ecomiles credited
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200': { description: Acknowledged }
  balance.updated:
    post:
      summary: User balance changed
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200': { description: Acknowledged }
  reward.redeemed:
    post:
      summary: Redemption completed (catalog or card)
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200': { description: Acknowledged }
  inventory.completed:
    post:
      summary: Inventory results available
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200': { description: Acknowledged }
  campaign.ended:
    post:
      summary: Campaign finished
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200': { description: Acknowledged }
  goal.achieved:
    post:
      summary: Impact goal reached
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200': { description: Acknowledged }

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Per-organization API key (server-to-server)
    oauthClientCredentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://auth.ecomilhas.com/oauth/token
          scopes:
            trips:write: Register trips
            balance:read: Read balances and transactions
            impact:read: Read organizational impact
            inventory:write: Manage inventories
    oauthAuthCode:
      type: oauth2
      description: '"Connect with Ecomilhas" — end-user consent flow'
      flows:
        authorizationCode:
          authorizationUrl: https://auth.ecomilhas.com/oauth/authorize
          tokenUrl: https://auth.ecomilhas.com/oauth/token
          scopes:
            balance:read: Read the user's balance and statement
            trips:write: Submit trips on the user's behalf
            wallet:provision: Provision the user's card to a device wallet

  parameters:
    UserId:
      name: user_id
      in: path
      required: true
      schema: { type: string }
    OrgId:
      name: org_id
      in: path
      required: true
      schema: { type: string }
    InventoryId:
      name: inventory_id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: Prevents duplicates on accidental retries
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 50, maximum: 200 }

  responses:
    BadRequest:
      description: Invalid payload or missing required fields
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: No permission for this organization/user
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: Idempotency conflict or duplicated external id
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unprocessable:
      description: Semantic validation error (e.g. unsupported unit or modal)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: VALIDATION_ERROR }
            message: { type: string }
            details:
              type: array
              items: { type: object }

    Balance:
      type: object
      properties:
        user_id: { type: string, example: usr_8a1b2c }
        balance: { type: number, example: 1250.5 }
        currency: { type: string, const: ECOMILES }
        lifetime_earned: { type: number, example: 4890.0 }
        co2e_avoided_kg_lifetime: { type: number, example: 312.4 }
        updated_at: { type: string, format: date-time }

    Transaction:
      type: object
      properties:
        transaction_id: { type: string, example: txn_44ab21 }
        user_id: { type: string }
        type:
          type: string
          enum: [EARN, CREDIT, REDEEM, EXPIRE, ADJUST]
        amount: { type: number }
        balance_after: { type: number }
        reason: { type: string, example: CAMPAIGN_BONUS }
        external_ref: { type: string }
        created_at: { type: string, format: date-time }

    CreditRequest:
      type: object
      required: [amount]
      properties:
        amount: { type: number, exclusiveMinimum: 0, example: 100 }
        reason: { type: string, example: CAMPAIGN_BONUS }
        external_ref: { type: string, example: CAMP-2026-07-001 }

    Modal:
      type: string
      enum: [WALK, BICYCLE, E_BIKE, SCOOTER, EV_MOTORCYCLE, EV_CAR, CARPOOL, BUS, RAIL, METRO]

    GeoPoint:
      type: object
      required: [lat, lng]
      properties:
        lat: { type: number, minimum: -90, maximum: 90 }
        lng: { type: number, minimum: -180, maximum: 180 }

    TripRequest:
      type: object
      required: [external_trip_id, user_id, modal, distance_km, started_at, ended_at]
      properties:
        external_trip_id: { type: string, example: VW-TRIP-20260730-001 }
        user_id: { type: string }
        modal: { $ref: '#/components/schemas/Modal' }
        distance_km: { type: number, exclusiveMinimum: 0, example: 18.4 }
        started_at: { type: string, format: date-time }
        ended_at: { type: string, format: date-time }
        source:
          type: string
          enum: [VEHICLE_TELEMETRY, DELIVERY_PLATFORM, SDK_AUTO, MANUAL]
        origin: { $ref: '#/components/schemas/GeoPoint' }
        destination: { $ref: '#/components/schemas/GeoPoint' }
        org_id:
          type: string
          description: Organization to attribute the trip's impact to (optional)

    TripResult:
      type: object
      properties:
        trip_id: { type: string, example: trp_f31a9d }
        external_trip_id: { type: string }
        status:
          type: string
          enum: [PROCESSED, PENDING_VALIDATION, REJECTED]
        ecomiles_earned: { type: number, example: 5.52 }
        co2e_avoided_kg: { type: number, example: 2.87 }
        calculation:
          type: object
          properties:
            ecomiles_per_km: { type: number, example: 0.3 }
            baseline_modal: { type: string, example: ICE_CAR }
            region: { type: string, example: SOUTH_AMERICA }
        rejection_reason:
          type: [string, 'null']
          example: SPEED_INCONSISTENT_WITH_MODAL

    ImpactReport:
      type: object
      properties:
        org_id: { type: string }
        period:
          type: object
          properties:
            start: { type: string, format: date }
            end: { type: string, format: date }
        totals:
          type: object
          properties:
            trips: { type: integer }
            distance_km: { type: number }
            ecomiles_issued: { type: number }
            co2e_avoided_tonnes: { type: number }
        by_modal:
          type: array
          items:
            type: object
            properties:
              modal: { $ref: '#/components/schemas/Modal' }
              trips: { type: integer }
              distance_km: { type: number }
              co2e_avoided_tonnes: { type: number }

    GhgExport:
      type: object
      properties:
        org_id: { type: string }
        methodology: { type: string, example: GHG_PROTOCOL }
        emission_factors_version: { type: string, example: 2026.1 }
        period:
          type: object
          properties:
            start: { type: string, format: date }
            end: { type: string, format: date }
        entries:
          type: array
          items:
            type: object
            properties:
              scope:
                type: string
                enum: [SCOPE_1, SCOPE_2, SCOPE_3]
              category: { type: string, example: 'SCOPE3_CAT7_EMPLOYEE_COMMUTING' }
              activity: { type: string, example: 'Commuting by bicycle' }
              quantity: { type: number }
              unit: { type: string, example: km }
              co2e_tonnes: { type: number }
              type:
                type: string
                enum: [EMISSION, AVOIDED_EMISSION]

    CampaignRequest:
      type: object
      required: [name, starts_at, ends_at]
      properties:
        name: { type: string, example: 'Matchday Verde — ATM x RMA' }
        starts_at: { type: string, format: date-time }
        ends_at: { type: string, format: date-time }
        multiplier: { type: number, example: 2.0 }
        eligible_modals:
          type: array
          items: { $ref: '#/components/schemas/Modal' }
        geofence:
          type: object
          description: Optional area where trips must end (e.g. stadium)
          properties:
            center: { $ref: '#/components/schemas/GeoPoint' }
            radius_m: { type: integer, example: 1500 }
        goal_co2e_tonnes: { type: number }

    Campaign:
      allOf:
        - $ref: '#/components/schemas/CampaignRequest'
        - type: object
          properties:
            campaign_id: { type: string, example: cmp_77fe10 }
            status:
              type: string
              enum: [SCHEDULED, ACTIVE, ENDED, CANCELLED]

    Member:
      type: object
      properties:
        member_id: { type: string }
        external_member_id: { type: string }
        user_id: { type: [string, 'null'] }
        role:
          type: string
          enum: [EMPLOYEE, COURIER, FAN, DRIVER]
        status:
          type: string
          enum: [INVITED, ACTIVE, REMOVED]

    InventoryRequest:
      type: object
      required: [external_id, organization, period_start, period_end]
      properties:
        external_id: { type: string, example: INV-CLIENTE-2026-01 }
        organization:
          type: object
          required: [external_org_id]
          properties:
            external_org_id: { type: string, example: ORG-123 }
            name: { type: string, example: Empresa Exemplo LTDA }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date }

    Inventory:
      type: object
      properties:
        inventory_id: { type: string, example: inv_9f2c1a }
        external_id: { type: string }
        status:
          $ref: '#/components/schemas/InventoryLifecycle'
        created_at: { type: string, format: date-time }

    InventoryLifecycle:
      type: string
      enum: [DRAFT, RECEIVED, PROCESSING, COMPLETED, FAILED]

    InventoryStatus:
      type: object
      properties:
        inventory_id: { type: string }
        status: { $ref: '#/components/schemas/InventoryLifecycle' }
        items_total: { type: integer }
        items_valid: { type: integer }
        items_invalid: { type: integer }

    InventoryItem:
      type: object
      required: [external_item_id, scope, category, quantity, unit]
      properties:
        external_item_id: { type: string, example: LINE-001 }
        scope:
          type: string
          enum: [SCOPE_1, SCOPE_2, SCOPE_3]
        category: { type: string, example: ELECTRICITY }
        quantity: { type: number, example: 12000 }
        unit: { type: string, example: kWh }

    InventoryResults:
      type: object
      properties:
        inventory_id: { type: string }
        status: { $ref: '#/components/schemas/InventoryLifecycle' }
        totals:
          type: object
          properties:
            tco2e_total: { type: number, example: 82.41 }
        by_scope:
          type: array
          items:
            type: object
            properties:
              scope: { type: string }
              tco2e: { type: number }

    WebhookEnvelope:
      type: object
      required: [event, event_id, created_at, data]
      properties:
        event:
          type: string
          enum: [trip.processed, balance.updated, reward.redeemed, inventory.completed, campaign.ended, goal.achieved]
        event_id: { type: string, example: evt_9c81aa }
        created_at: { type: string, format: date-time }
        data:
          type: object
          description: Event-specific payload (TripResult, Balance, InventoryResults, Campaign...)
      description: |
        Delivered with `X-Ecomilhas-Signature: sha256=<HMAC-SHA256 hex of raw body>`.
        Retries with exponential backoff for up to 24h until a 2xx is returned.
