openapi: 3.1.0
info:
  title: Tolls API
  version: 1.0.0
  description: >
    Cargoffer Tolls SaaS API for account, wallet, API-key, toll-calculation proxy,
    usage history, temporary agent trials, human agent approval, and wallet subscriptions.
    This is the v1.0.0 contract. Public endpoints use the existing unversioned `/api`
    prefix; `info.version` is not a `/v1` path segment. No operations are marked deprecated
    in this contract revision.
  contact:
    name: Cargoffer
    url: https://cargoffer.com
servers:
  - url: https://api.tolls.cargoffer.com
    description: Production
security:
  - jwtBearer: []
  - apiKeyBearer: []
tags:
  - name: Auth
    description: Sign-in, token refresh, and token validation.
  - name: Account
    description: Authenticated account data export and account deletion.
  - name: Onboarding
    description: Dashboard start-up checklist and its one-off credit reward.
  - name: Wallet
    description: Credits, top-ups, coupon redemption, invoices, and subscriptions.
  - name: Keys
    description: API-key creation, listing, and revocation.
  - name: Agents
    description: Temporary agent trials and human approval workflows.
  - name: Tolls
    description: Toll-calculation requests and toll-engine operations.
  - name: Catalog
    description: Operator and tariff data forwarded from the configured toll engine.
  - name: History
    description: Paginated request and usage history.
  - name: System
    description: Service health and runtime status.
components:
  securitySchemes:
    jwtBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Dashboard session bearer (application JWT or verified Clerk session when configured).
    apiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: API_KEY
      description: API key (starts with `tk_live_`); agent keys may be scoped and revocable.
    trialBearer:
      type: http
      scheme: bearer
      bearerFormat: TollsTrialToken
      description: Temporary agent trial token, valid for up to 24 hours.
  schemas:
    Address:
      type: object
      required:
        - id
        - label
        - name
        - lat
        - lng
      properties:
        id:
          type: string
        label:
          type: string
          maxLength: 80
        name:
          type: string
          maxLength: 160
        detail:
          type: string
          maxLength: 240
        city:
          type: string
        province:
          type: string
        country:
          type: string
        lat:
          type: number
        lng:
          type: number
        source:
          type: string
          enum:
            - cityLookup
            - poi
            - map
            - manual
        useCount:
          type: integer
        lastUsedAt:
          type:
            - string
            - 'null'
        createdAt:
          type:
            - string
            - 'null'
    AddressInput:
      type: object
      required:
        - name
        - lat
        - lng
      properties:
        label:
          type: string
          maxLength: 80
        name:
          type: string
          maxLength: 160
        detail:
          type: string
          maxLength: 240
        city:
          type: string
        province:
          type: string
        country:
          type: string
        lat:
          type: number
          minimum: -90
          maximum: 90
        lng:
          type: number
          minimum: -180
          maximum: 180
        source:
          type: string
          enum:
            - cityLookup
            - poi
            - map
            - manual
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: [string, 'null']
    AccountDataExport:
      type: object
      required: [exportVersion, exportedAt, account, wallet, invoices, billingHistory, subscriptions, ledger, truncated]
      properties:
        exportVersion:
          type: integer
          const: 1
        exportedAt:
          type: string
          format: date-time
        account:
          type: object
          required: [email, name, plan, createdAt]
          properties:
            email:
              type: string
              format: email
            name:
              type: string
            plan:
              type: string
            createdAt:
              type: string
              format: date-time
        wallet:
          type: [object, 'null']
          properties:
            balance:
              type: number
            totalPurchased:
              type: number
            totalConsumed:
              type: number
            totalBonus:
              type: number
        invoices:
          type: array
          items:
            type: object
            required: [amountEuros, tokensCredited, packLabel, status, receiptUrl, createdAt, paidAt]
            properties:
              amountEuros:
                type: number
              tokensCredited:
                type: integer
              packLabel:
                type: string
              status:
                type: string
                enum: [pending, paid, failed, refunded]
              receiptUrl:
                type: [string, 'null']
                format: uri
              createdAt:
                type: string
                format: date-time
              paidAt:
                type: [string, 'null']
                format: date-time
        billingHistory:
          type: array
          items:
            $ref: '#/components/schemas/Recharge'
        subscriptions:
          type: array
          items:
            $ref: '#/components/schemas/AccountExportSubscription'
        ledger:
          type: array
          items:
            $ref: '#/components/schemas/AccountExportLedger'
        truncated:
          type: object
          required: [invoices, billingHistory, subscriptions, ledger]
          properties:
            invoices:
              type: boolean
            billingHistory:
              type: boolean
            subscriptions:
              type: boolean
            ledger:
              type: boolean
    AccountExportSubscription:
      type: object
      required: [packEuros, tokensPerPeriod, status, cancelAtPeriodEnd, currentPeriodEnd, createdAt]
      properties:
        packEuros:
          type: number
        tokensPerPeriod:
          type: integer
        status:
          type: string
          enum: [incomplete, trialing, active, past_due, canceled, unpaid]
        cancelAtPeriodEnd:
          type: boolean
        currentPeriodEnd:
          type: [string, 'null']
          format: date-time
        createdAt:
          type: string
          format: date-time
    AccountExportLedger:
      type: object
      required: [type, amountTokens, referenceId, metadata, createdAt]
      properties:
        type:
          type: string
          enum: [purchase, subscription, bonus, usage, refund]
        amountTokens:
          type: integer
        referenceId:
          type: [string, 'null']
        metadata:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
    AccountDeletionRequest:
      type: object
      required: [confirmationEmail]
      properties:
        confirmationEmail:
          type: string
          format: email
    AccountDeletionResult:
      type: object
      required: [ok, revokedKeys, canceledSubscriptions, identityProviderDeleted]
      properties:
        ok:
          type: boolean
          const: true
        revokedKeys:
          type: integer
        canceledSubscriptions:
          type: integer
        identityProviderDeleted:
          type: boolean
          const: false
    Tokens:
      type: object
      properties:
        accessToken:
          type: string
        refreshToken:
          type: string
    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string
        details:
          type: object
          additionalProperties: true
    Wallet:
      type: object
      properties:
        balance:
          type: number
        totalPurchased:
          type: number
        totalConsumed:
          type: number
        totalBonus:
          type: number
          description: Credits granted as bonuses, including human-approved agent onboarding.
        packs:
          type: array
          items:
            type: object
            properties:
              euros:
                type: number
              tokens:
                type: integer
        customRate:
          type: number
          description: Tokens awarded per euro for custom wallet recharges.
    Recharge:
      type: object
      description: Wallet recharge record.
      properties:
        amountEuros:
          type: number
        tokensCredited:
          type: integer
        packLabel:
          type: string
        status:
          type: string
          enum: [pending, paid, failed, refunded]
        receiptUrl:
          type: [string, 'null']
          format: uri
        createdAt:
          type: string
          format: date-time
        paidAt:
          type: [string, 'null']
          format: date-time
        source:
          type: string
          enum: [purchase, subscription]
        stripeInvoiceId:
          type: [string, 'null']
          description: Stripe invoice ID for a subscription credit, when applicable.
    Checkout:
      type: object
      properties:
        checkoutUrl:
          type: [string, 'null']
          format: uri
        sessionId:
          type: string
        tokens:
          type: integer
    ApiKey:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        keyPreview:
          type: string
        active:
          type: boolean
        keyType:
          type: string
          enum: [user, agent]
        scopes:
          type: array
          items:
            type: string
          description: Agent keys are limited to tolls:calculate and tolls:read by default.
        x402Enabled:
          type: boolean
        expiresAt:
          type: [string, 'null']
          format: date-time
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: [string, 'null']
          format: date-time
    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          properties:
            token:
              type: string
              description: Full secret key (only returned once at creation).
    ApiKeyLimits:
      type: object
      required: [maxActive]
      properties:
        maxActive:
          type: integer
          description: >-
            Maximum number of **active** keys per account. Revoking a key frees a
            slot, because revocation deactivates the key instead of deleting it.
          example: 10
    TourState:
      type: object
      required: [steps, completed, reward, balance, actions]
      description: >-
        Onboarding checklist state. Every step is derived server-side from real
        data (active keys, paid invoices, successful calculations), so it cannot be
        forged from the browser.
      properties:
        steps:
          type: object
          required: [key, recharge, calculate]
          properties:
            key:
              type: boolean
              description: The account has at least one active API key.
            recharge:
              type: boolean
              description: The account has at least one paid invoice.
            calculate:
              type: boolean
              description: The account has at least one successful calculation.
        completed:
          type: boolean
          description: All three steps are done.
        reward:
          type: object
          required: [tokens, claimed]
          properties:
            tokens:
              type: integer
              description: Credits granted once when the checklist is completed.
              example: 25
            claimed:
              type: boolean
              description: The reward was already claimed.
        balance:
          type: integer
          description: Current credit balance.
        actions:
          type: object
          description: Where the dashboard should navigate for each step.
          properties:
            key:
              type: string
            recharge:
              type: string
            calculate:
              type: string
    CalculateRequest:
      type: object
      additionalProperties: true
      description: >
        JSON object forwarded to the configured toll engine. This API currently
        validates only that the body is an object; field-level contract depends
        on the configured upstream and must be confirmed before publication.
    CalculateResponse:
      description: JSON payload forwarded from the configured toll engine.
    Operator:
      type: object
      description: Engine-specific operator object forwarded by the API.
      additionalProperties: true
    Tariff:
      type: object
      description: Engine-specific tariff object forwarded by the API.
      additionalProperties: true
    HistoryItem:
      type: object
      properties:
        _id:
          type: string
        endpoint:
          type: string
        method:
          type: string
        source:
          type: string
          enum: [dashboard, apikey, demo]
        apiKeyId:
          type: [string, 'null']
        tokensCharged:
          type: integer
        statusCode:
          type: integer
        latencyMs:
          type: number
        createdAt:
          type: string
          format: date-time
    HistoryPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/HistoryItem'
        total:
          type: integer
        page:
          type: integer
        pages:
          type: integer
    Status:
      type: object
      properties:
        engine:
          type: string
          enum: [up, down]
        upstream:
          description: Upstream health payload, or null when unavailable.
    Health:
      type: object
      properties:
        status:
          type: string
          example: ok
        service:
          type: string
        time:
          type: string
          format: date-time
    WalletSubscription:
      type: object
      required: [stripeSubscriptionId, packEuros, tokensPerPeriod, status, cancelAtPeriodEnd, currentPeriodEnd]
      properties:
        stripeSubscriptionId:
          type: string
        packEuros:
          type: number
          enum: [2, 5, 10, 50]
        tokensPerPeriod:
          type: integer
        status:
          type: string
          enum: [incomplete, trialing, active, past_due, canceled, unpaid]
        cancelAtPeriodEnd:
          type: boolean
        currentPeriodEnd:
          type: [string, 'null']
          format: date-time
    SubscriptionCheckout:
      type: object
      required: [checkoutUrl, sessionId, tokens, recurring]
      properties:
        checkoutUrl:
          type: [string, 'null']
          format: uri
        sessionId:
          type: string
        tokens:
          type: integer
        recurring:
          type: boolean
          const: true
    AgentTrialCreated:
      type: object
      required: [trialId, trialToken, successfulCalculationsRemaining, expiresAt]
      properties:
        trialId:
          type: string
        trialToken:
          type: string
          description: Secret temporary bearer token; store securely and never log it.
        successfulCalculationsRemaining:
          type: integer
          const: 3
        expiresAt:
          type: string
          format: date-time
    AgentTrialCalculateRequest:
      type: object
      required: [origin, destination]
      properties:
        origin:
          type: object
          additionalProperties: true
        destination:
          type: object
          additionalProperties: true
      additionalProperties: true
    AgentTrialCalculationResponse:
      type: object
      required: [result, usage]
      properties:
        result:
          description: Toll-engine response for the successful calculation.
        usage:
          type: object
          required: [successfulCalculationsUsed, successfulCalculationsRemaining]
          properties:
            successfulCalculationsUsed:
              type: integer
            successfulCalculationsRemaining:
              type: integer
    AgentRegistrationCreated:
      type: object
      required: [registrationId, pairingCode, expiresAt, successfulCalculationsRequired, dashboardUrl]
      properties:
        registrationId:
          type: string
        pairingCode:
          type: string
          pattern: '^[A-F0-9]{10}$'
          description: One-time human-approval code; expires after 15 minutes.
        expiresAt:
          type: string
          format: date-time
        successfulCalculationsRequired:
          type: integer
          const: 3
        dashboardUrl:
          type: string
          format: uri
    AgentApprovalRequest:
      type: object
      required: [pairingCode, x402Enabled]
      properties:
        pairingCode:
          type: string
          pattern: '^[A-F0-9]{10}$'
        x402Enabled:
          type: boolean
          description: False by default; x402 settlement is not implemented in this release.
    AgentApprovalResult:
      type: object
      required: [registrationId, ownerUserId, agent, apiKeyId, keyPreview, scopes, x402Enabled, token, bonusCredits, tokenShownOnce]
      properties:
        registrationId:
          type: string
        ownerUserId:
          type: string
        agent:
          type: string
        apiKeyId:
          type: string
        keyPreview:
          type: string
        scopes:
          type: array
          items:
            type: string
          const: ['tolls:calculate', 'tolls:read']
        x402Enabled:
          type: boolean
          description: Indicates whether x402 was explicitly enabled for this approved agent key.
        token:
          type: string
          description: Secret API key returned once; deliver it to the agent through a secure channel.
        bonusCredits:
          type: integer
          const: 25
        tokenShownOnce:
          type: boolean
          const: true
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
paths:
  /api/auth/register:
    post:
      tags: [Auth]
      summary: Register a new account
      operationId: register
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
                name:
                  type: string
      responses:
        '201':
          description: Account created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/User'
                  accessToken:
                    type: string
                  refreshToken:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
  /api/auth/login:
    post:
      tags: [Auth]
      summary: Authenticate and obtain tokens
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        '200':
          description: Login successful.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tokens'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/auth/refresh:
    post:
      tags: [Auth]
      summary: Refresh an access token
      operationId: refresh
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
      responses:
        '200':
          description: New tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tokens'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/account/export:
    get:
      tags: [Account]
      summary: Export the current account data
      operationId: getAccountExport
      security:
        - jwtBearer: []
      responses:
        '200':
          description: JSON export of profile, wallet, invoices, billing history, subscriptions and ledger; never cached.
          headers:
            Cache-Control:
              schema:
                type: string
                example: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountDataExport'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No active account was found.
  /api/account:
    delete:
      tags: [Account]
      summary: Anonymize and deactivate the current Tolls account
      description: >
        Requires the exact account email as a second confirmation. Immediately
        cancels active Stripe subscriptions without proration or automatic refund,
        revokes API keys, and anonymizes the local Tolls profile. Pending Stripe
        invoice items may still be charged at period end. Wallet
        balance, invoices and ledger entries are retained. The Clerk identity is
        not deleted until its tenant is verified as exclusive to Tolls.
      operationId: deleteAccount
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccountDeletionRequest'
      responses:
        '200':
          description: Account anonymized; billing records retained and keys revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountDeletionResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No active account was found.
        '409':
          description: Account state changed while deletion was in progress.
        '502':
          description: Stripe could not confirm subscription cancellation; the Tolls profile remains active.
        '503':
          description: Stripe is unavailable while an active subscription must be canceled.
  /api/wallet/coupons:
    post:
      tags: [Wallet]
      summary: Create a wallet coupon (admin)
      operationId: createCoupon
      security: []
      parameters:
        - name: x-admin-key
          in: header
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, tokens]
              properties:
                code: { type: string, minLength: 4, maxLength: 64 }
                tokens: { type: integer, minimum: 1 }
                maxRedemptions: { type: integer, minimum: 1 }
                expiresAt: { type: string, format: date-time }
                allowedEmail: { type: string, format: email }
      responses:
        '201':
          description: Coupon created.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Coupon code already exists.
  /api/wallet/redeem:
    post:
      tags: [Wallet]
      summary: Redeem a coupon into the wallet
      operationId: redeemCoupon
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, minLength: 4, maxLength: 64 }
      responses:
        '200':
          description: Coupon redeemed; returns tokens credited and new balance.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not found, inactive, expired, exhausted or email mismatch.
        '409':
          description: Already redeemed by this account.
  /api/auth/validate:
    get:
      tags: [Auth]
      summary: Validate the current token and return identity/scopes
      operationId: validateToken
      security:
        - jwtBearer: []
        - apiKeyBearer: []
      responses:
        '200':
          description: Token is valid; returns subject, token type, auth source and scopes.
          content:
            application/json:
              schema:
                type: object
                required: [valid, subject, tokenType, authSource, scopes]
                properties:
                  valid:
                    type: boolean
                  subject:
                    type: string
                  tokenType:
                    type: string
                    enum: [session, api_key]
                  authSource:
                    type: string
                    enum: [dashboard, apikey, demo]
                  scopes:
                    type: array
                    items:
                      type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/wallet:
    get:
      tags: [Wallet]
      summary: Get wallet balance and packs
      operationId: getWallet
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Wallet snapshot.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Wallet'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/wallet/recharges:
    get:
      tags: [Wallet]
      summary: List wallet recharges
      operationId: getRecharges
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Recharge history.
          content:
            application/json:
              schema:
                type: object
                required: [recharges]
                properties:
                  recharges:
                    type: array
                    items:
                      $ref: '#/components/schemas/Recharge'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/wallet/subscription:
    get:
      tags: [Wallet]
      summary: Get the current wallet subscription
      operationId: getWalletSubscription
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Current subscription, or null when the account has none.
          content:
            application/json:
              schema:
                type: object
                required: [subscription]
                properties:
                  subscription:
                    oneOf:
                      - $ref: '#/components/schemas/WalletSubscription'
                      - type: 'null'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/wallet/checkout:
    post:
      tags: [Wallet]
      summary: Create a Stripe checkout session
      operationId: checkout
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [euros]
              properties:
                euros:
                  type: number
                  minimum: 1
                  maximum: 500
      responses:
        '201':
          description: Checkout session created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Checkout'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Payment provider is not configured.
  /api/wallet/subscription/checkout:
    post:
      tags: [Wallet]
      summary: Start an optional monthly wallet subscription
      operationId: createWalletSubscriptionCheckout
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [euros]
              properties:
                euros:
                  type: number
                  enum: [2, 5, 10, 50]
                  description: Existing wallet pack charged monthly; credits accumulate and do not expire.
      responses:
        '201':
          description: Stripe subscription checkout session created; credits are granted only after invoice.paid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionCheckout'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: An active or pending subscription already exists.
        '503':
          description: Payment provider is not configured.
  /api/wallet/subscription/cancel:
    post:
      tags: [Wallet]
      summary: Cancel renewal at the end of the paid period
      operationId: cancelWalletSubscription
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Renewal cancellation scheduled; existing credits remain available.
          content:
            application/json:
              schema:
                type: object
                required: [subscription, message]
                properties:
                  subscription:
                    $ref: '#/components/schemas/WalletSubscription'
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No cancellable subscription was found.
        '503':
          description: Payment provider is not configured.
  /api/agents/trials:
    post:
      tags: [Agents]
      summary: Create a temporary agent trial
      operationId: createAgentTrial
      security: []
      description: Creates a 24-hour trial with three successful toll calculations.
      responses:
        '201':
          description: Trial token issued. Keep it secret; it is needed for trial calls.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentTrialCreated'
        '429':
          description: Trial creation rate limit exceeded.
  /api/agents/trials/calculate:
    post:
      tags: [Agents]
      summary: Calculate tolls using an agent trial
      operationId: calculateAgentTrial
      security:
        - trialBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentTrialCalculateRequest'
      responses:
        '200':
          description: Successful trial calculation; only successful calculations consume quota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentTrialCalculationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The three successful trial calculations have been used.
        '429':
          description: Calculation already in progress or rate limit exceeded.
        '502':
          description: Toll engine unavailable; quota is restored.
  /api/agents/registrations:
    post:
      tags: [Agents]
      summary: Request human approval after completing the trial
      operationId: registerAgentForApproval
      security:
        - trialBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agentName]
              properties:
                agentName:
                  type: string
                  minLength: 1
                  maxLength: 80
      responses:
        '201':
          description: One-time pairing code issued for human approval.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentRegistrationCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Trial is not exhausted or the pairing request is invalid.
        '429':
          description: Trial calculations are still in progress or rate limit exceeded.
  /api/agents/registrations/approve:
    post:
      tags: [Agents]
      summary: Approve an agent and issue a scoped API key
      operationId: approveAgentRegistration
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentApprovalRequest'
      responses:
        '201':
          description: Agent approved; scoped key and 25 bonus credits issued atomically. The token is shown once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentApprovalResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Pairing code invalid, expired, used, x402 unavailable, or API-key limit reached.
  /api/webhooks/stripe:
    post:
      tags: [Wallet]
      summary: Stripe webhook (raw body + signature)
      operationId: stripeWebhook
      security: []
      description: >
        Receives Stripe events. The raw request body must be preserved and the
        `stripe-signature` header validated before processing.
      parameters:
        - name: stripe-signature
          in: header
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Event processed.
        '400':
          description: Invalid signature or payload.
  /api/keys:
    get:
      tags: [Keys]
      summary: List API keys
      operationId: listKeys
      security:
        - jwtBearer: []
      responses:
        '200':
          description: API keys.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKey'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Keys]
      summary: Create an API key
      operationId: createKey
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        '201':
          description: Key created (token shown once).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Maximum number of API keys reached.
  /api/keys/limits:
    get:
      tags: [Keys]
      summary: Active-key limit for this account
      description: >-
        Returns the cap on **active** keys. The dashboard uses it to show the usage
        counter and to disable the create form before the API answers 409.
      operationId: getKeyLimits
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Active-key limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyLimits'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/tour:
    get:
      tags: [Onboarding]
      summary: Onboarding checklist state
      description: >-
        Steps are computed from real account data (active keys, paid invoices,
        successful calculations). `actions` tells the dashboard where to navigate
        for each pending step.
      operationId: getTour
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Checklist state and reward status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TourState'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/tour/reward:
    post:
      tags: [Onboarding]
      summary: Claim the onboarding reward
      description: >-
        Grants the one-off credit reward once every step is complete. Idempotent:
        a second call never credits twice and reports `alreadyClaimed`.
      operationId: claimTourReward
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Reward outcome.
          content:
            application/json:
              schema:
                type: object
                required: [claimed, tokens, balance]
                properties:
                  claimed:
                    type: boolean
                    description: True only when this call granted the credits.
                  tokens:
                    type: integer
                    description: Credits granted by this call (0 if not granted).
                  balance:
                    type: integer
                    description: Credit balance after the call.
                  alreadyClaimed:
                    type: boolean
                    description: The reward had already been claimed earlier.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Checklist is not complete yet (`TOUR_INCOMPLETE`).
  /api/keys/{id}:
    delete:
      tags: [Keys]
      summary: Revoke an API key
      operationId: deleteKey
      security:
        - jwtBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Key revoked.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not found.
  /api/geo/search:
    get:
      tags:
        - Geo
      summary: Search places for the calculator autocomplete
      description: >-
        Proxies the TRANSCEND POI unified search so the browser never sees the
        internal token. Results are normalized (name, detail, city, country,
        lat, lng), deduplicated and ranked with POIs first and ES/PT/FR ahead of
        other countries.
      operationId: searchPlaces
      security:
        - jwtBearer: []
      parameters:
        - name: q
          in: query
          required: true
          description: Free text, at least 2 characters.
          schema:
            type: string
            minLength: 2
        - name: country
          in: query
          required: false
          description: ISO-3166 alpha-2 filter (for example ES, PT, FR).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 8
      responses:
        '200':
          description: Normalized places.
          content:
            application/json:
              schema:
                type: object
                required:
                  - query
                  - places
                properties:
                  query:
                    type: string
                  places:
                    type: array
                    items:
                      type: object
                      required:
                        - id
                        - name
                        - lat
                        - lng
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        detail:
                          type: string
                        city:
                          type: string
                        province:
                          type: string
                        country:
                          type: string
                        lat:
                          type: number
                        lng:
                          type: number
                        source:
                          type: string
                          enum:
                            - cityLookup
                            - poi
        '400':
          description: Query shorter than 2 characters.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '502':
          description: The upstream place service failed.
        '503':
          description: The upstream place service is not configured or unreachable.

  /api/addresses:
    get:
      tags:
        - Addresses
      summary: List the user's saved addresses
      description: >-
        Frequent addresses used by the calculator, ordered by real usage
        (useCount) and then by recency. Maximum 50 per user.
      operationId: listAddresses
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Saved addresses.
          content:
            application/json:
              schema:
                type: object
                required:
                  - addresses
                  - limit
                properties:
                  limit:
                    type: integer
                  addresses:
                    type: array
                    items:
                      $ref: '#/components/schemas/Address'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Addresses
      summary: Save an address
      description: >-
        Returns 200 with the existing entry when the same name and coordinates
        are already saved; 201 when a new address is created. Fails with 409
        when the user already has 50 addresses.
      operationId: createAddress
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddressInput'
      responses:
        '200':
          description: Address already existed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  created:
                    type: boolean
                  address:
                    $ref: '#/components/schemas/Address'
        '201':
          description: Address created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  created:
                    type: boolean
                  address:
                    $ref: '#/components/schemas/Address'
        '400':
          description: Invalid payload.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Address book is full.

  /api/addresses/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      tags:
        - Addresses
      summary: Rename a saved address
      operationId: renameAddress
      security:
        - jwtBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - label
              properties:
                label:
                  type: string
                  maxLength: 80
      responses:
        '200':
          description: Updated address.
          content:
            application/json:
              schema:
                type: object
                properties:
                  address:
                    $ref: '#/components/schemas/Address'
        '400':
          description: Invalid id or label.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Address not found for this user.
    delete:
      tags:
        - Addresses
      summary: Delete a saved address
      operationId: deleteAddress
      security:
        - jwtBearer: []
      responses:
        '200':
          description: Address deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
        '400':
          description: Invalid id.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Address not found for this user.

  /api/addresses/{id}/use:
    post:
      tags:
        - Addresses
      summary: Mark an address as used
      description: Increments useCount so the address book orders by real usage.
      operationId: useAddress
      security:
        - jwtBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Updated address.
          content:
            application/json:
              schema:
                type: object
                properties:
                  address:
                    $ref: '#/components/schemas/Address'
        '400':
          description: Invalid id.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Address not found for this user.

  /api/tolls/calculate:
    post:
      tags: [Tolls]
      summary: Calculate tolls (1 token)
      operationId: calculate
      description: >
        Calculates toll cost for a route. A successful calculation charges 1 token;
        upstream failures and invalid upstream requests are refunded (idempotent
        refund via ledger mirror key). Requires either a JWT or an API key.
        Clients may send an `Idempotency-Key` to prevent duplicate engine calls
        and charges. Reusing a processed key returns 409; the API does not replay
        the original response. Persist the response with its key and use a new
        key for a new calculation.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >
            Client-generated key (8-128 ASCII letters, digits, `_`, `:`, `.`, or `-`).
            A processed key returns 409 without a second charge or engine call; the
            original response is not replayed.
          schema:
            type: string
            minLength: 8
            maxLength: 128
            pattern: '^[A-Za-z0-9_:.-]{8,128}$'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CalculateRequest'
      responses:
        '200':
          description: Calculation succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalculateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Insufficient tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Key already processed; no second charge or engine call is made, and the original response is not replayed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Wallet/ledger temporarily unavailable (fail-closed, retryable).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: Upstream toll provider error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/demo/calculate:
    post:
      tags: [Tolls]
      summary: Demo calculation (no auth, rate limited)
      operationId: demoCalculate
      security: []
      description: >
        Unauthenticated demo. Rate limits are configurable per deployment.
        Returns 429 when a configured limit is exhausted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CalculateRequest'
      responses:
        '200':
          description: Demo calculation succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalculateResponse'
        '400':
          description: Invalid request body.
        '429':
          description: Rate limit exhausted.
        '502':
          description: Upstream toll engine is unavailable.
  /api/operators:
    get:
      tags: [Catalog]
      summary: List toll operators
      operationId: listOperators
      description: Query parameters are forwarded to the configured toll engine.
      parameters:
        - name: country
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Operators payload forwarded from the configured toll engine.
          content:
            application/json:
              schema:
                description: JSON payload; exact schema depends on the configured engine.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '502':
          description: Upstream toll engine is unavailable.
  /api/tariffs:
    get:
      tags: [Catalog]
      summary: List toll tariffs
      operationId: listTariffs
      description: Query parameters are forwarded to the configured toll engine.
      parameters:
        - name: operator
          in: query
          schema:
            type: string
        - name: vehicle_type
          in: query
          description: Optional engine-specific vehicle type, forwarded upstream.
          schema:
            type: string
      responses:
        '200':
          description: Tariffs payload forwarded from the configured toll engine.
          content:
            application/json:
              schema:
                description: JSON payload; exact schema depends on the configured engine.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '502':
          description: Upstream toll engine is unavailable.
  /api/history:
    get:
      tags: [History]
      summary: Usage history
      operationId: listHistory
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [2xx, 4xx, 5xx]
        - name: from
          in: query
          description: Include requests created at or after this timestamp.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          description: Include requests created at or before this timestamp.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Paginated history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/status:
    get:
      tags: [System]
      summary: Engine status
      operationId: status
      responses:
        '200':
          description: Engine and upstream status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /health:
    get:
      tags: [System]
      summary: Health check
      operationId: health
      security: []
      responses:
        '200':
          description: Service healthy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /:
    get:
      tags: [System]
      summary: Service information
      operationId: root
      security: []
      responses:
        '200':
          description: Service name, version, and documentation path.
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  version:
                    type: string
                  docs:
                    type: string
                    format: uri
                    description: Absolute URL of the public Scalar documentation site.
                    example: https://docs.tolls.cargoffer.com/
