> ## Documentation Index
> Fetch the complete documentation index at: https://docs.teamfollowup.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use the public API base URL: https://api.teamfollowup.ai/api.
> Authenticate public API requests with Authorization: Bearer YOUR_API_KEY.
> Use product-owned terms: agents, campaigns, projects (GoHighLevel sub-accounts), contacts, calls, outcomes, skills, cadences, phone numbers.
> Scope requests by locationId rather than projectName.
> Read the guide linked from each API reference group before recommending an endpoint.

# Get analytics by lead age

> Returns the analytics summary split by how old each lead was when it reached TFU AI: the gap between the moment the sub-account's CRM created the lead and the moment it was sent to TFU AI.

- `fresh`: sent within 5 minutes of being created.
- `warm`: more than 5 minutes, under 3 days.
- `dbr`: 3 days or more, such as a lead from an old list being worked again.
- `unknown`: the stored times cannot place the lead in exactly one of the three. `unknownReasons` counts why. A lead is never guessed into the nearer segment.

Counted the same way as the analytics summary and over the same filters, so the segments add up to it. The one difference is that a disconnected project's archived totals are not included, because they carry no lead ages.

Allowed customer roles: `agency_admin`, `project_user`.

Required API key scope: `dashboard:read`.



## OpenAPI

````yaml /openapi.yaml get /api/dashboard/analytics/lead-age
openapi: 3.1.0
info:
  title: TFUAI Server API
  version: 1.0.0
  description: >-
    API reference for the TFUAI SaaS server.


    All published endpoints are mounted under `/api`. Responses use a standard
    envelope: `{ "success": true, ... }` on success and `{ "success": false,
    "error": "..." }` on failure.


    Product API modules are being launched module by module. This spec currently
    exposes only contracted public modules.


    Authenticate public API requests with an API key in the `Authorization`
    header: `Authorization: Bearer YOUR_API_KEY`.
servers:
  - url: https://api.teamfollowup.ai
    description: Production API origin. Public API paths are under /api.
security: []
tags:
  - name: Agents
    description: >-
      Build the conversation: identity and settings, script, lead context and
      split testing.
  - name: Analytics
    description: >-
      Understand call volume, pickup, booking and transfer metrics within the
      selected reporting scope.
  - name: Billing
    description: >-
      Balances, spend, purchases and sub-account rebilling. Reference and guides
      only; the builder chatbot has no billing playbook.
  - name: Calls
    description: >-
      Review completed calls, recordings, transcripts and campaign run history;
      distinguish observation from placing a new call.
  - name: Campaigns
    description: >-
      Configure what starts calls, who is eligible, calling windows, caller IDs,
      activation and the actions that follow each outcome.


      What runs after a call lands: for each outcome (booked, opted out, asked
      for a callback…), an ordered chain of actions — tag the contact, move a
      pipeline stage, send an SMS, book an appointment. Stored as a graph:
      `nodes` are the actions, `edges` say what follows what, and
      `dispositionEntries` maps each outcome to the node its chain starts at.


      **Pick the narrowest endpoint that does the job.**


      | To | Call | |

      | --- | --- | --- |

      | See what fires per outcome | `GET .../workflow?view=summary` | Reads as
      plain text, no graph walking |

      | Add steps to an outcome | `POST .../outcomes/{outcome}/nodes` | Server
      derives node ids, edge ids, handles, layout |

      | Change a message, tag or setting | `PATCH .../workflow` | Merges;
      carries no graph, so it cannot damage one |

      | Remove one step | `DELETE .../nodes/{nodeId}` | Also re-links the chain
      around it |

      | Author or replace the whole graph | `PUT .../workflow` | Replaces
      everything you send |


      `PUT` is the only one that can re-wire, position nodes, build an `if`,
      save an unconfigured draft, or copy a whole workflow — and the only one
      that can overwrite a change someone else made after you read it. Use it
      when you are genuinely authoring the graph, and send `expectedVersion`
      when you do. For everything else the narrower endpoints are both easier
      and safer.
  - name: Contacts
    description: >-
      Find contacts and their calls, manage do-not-call status and identify
      internal contacts excluded from reporting.
  - name: In-call Capabilities
    description: >-
      Configure actions and instructions the agent uses during a live call:
      booking, transfers, lookups and custom skills.
  - name: Master agent
    description: >-
      Run a shared agent and campaign across linked projects; manage
      inheritance, local bindings, synchronization and rollout readiness.
  - name: Native Calendar
    description: >-
      The diary of a sub-account with no CRM: its bookable calendars, the free
      slots they offer, and the appointments on them. Availability is computed
      per request against live bookings and is never cached, because a cached
      slot is one somebody else has already taken.
  - name: Native Contacts
    description: >-
      The people a sub-account with no CRM calls, and the fields it keeps on
      them. This is the system of record for those contacts: there is nowhere
      else they exist. A sub-account with a CRM keeps its contacts there and
      every operation here answers 400 for it.
  - name: Phone Numbers
    description: >-
      Find and manage caller IDs, assign numbers to campaigns and understand
      connected Twilio numbers.
  - name: Projects
    description: >-
      Understand project ownership, inspect configuration and validate the local
      resources used by a campaign.
  - name: TFU Live
    description: >-
      Build one TFU Live agent with its Brain, Voice and equipped actions;
      connect it through shared Campaigns.
  - name: Voices
    description: >-
      Browse the voice catalogue and select a voice; configure speech and
      listening behavior in Agents.
paths:
  /api/dashboard/analytics/lead-age:
    get:
      tags:
        - Analytics
      summary: Get analytics by lead age
      description: >-
        Returns the analytics summary split by how old each lead was when it
        reached TFU AI: the gap between the moment the sub-account's CRM created
        the lead and the moment it was sent to TFU AI.


        - `fresh`: sent within 5 minutes of being created.

        - `warm`: more than 5 minutes, under 3 days.

        - `dbr`: 3 days or more, such as a lead from an old list being worked
        again.

        - `unknown`: the stored times cannot place the lead in exactly one of
        the three. `unknownReasons` counts why. A lead is never guessed into the
        nearer segment.


        Counted the same way as the analytics summary and over the same filters,
        so the segments add up to it. The one difference is that a disconnected
        project's archived totals are not included, because they carry no lead
        ages.


        Allowed customer roles: `agency_admin`, `project_user`.


        Required API key scope: `dashboard:read`.
      operationId: get-lead-age-analytics
      parameters:
        - name: dateFromUTC
          in: query
          required: false
          schema:
            type: string
          description: >-
            Start of the reporting window. A UTC ISO 8601 timestamp is
            canonical; a plain `YYYY-MM-DD` date is accepted and read as that
            day at 00:00:00Z. A timestamp must carry `Z` or an explicit offset,
            since one without a zone would be read as server-local time and
            shift the window. An empty string clears the bound. Anything else is
            a 400 rather than a silently wrong window.
          example: '2026-07-01T00:00:00.000Z'
        - name: dateToUTC
          in: query
          required: false
          schema:
            type: string
          description: >-
            End of the reporting window, inclusive. A UTC ISO 8601 timestamp is
            canonical; a plain `YYYY-MM-DD` date is accepted and covers that
            whole day, through 23:59:59.999Z. A timestamp must carry `Z` or an
            explicit offset, since one without a zone would be read as
            server-local time and shift the window. An empty string clears the
            bound. Anything else is a 400 rather than a silently wrong window.
          example: '2026-07-31T23:59:59.999Z'
        - name: projectName
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: >-
            Optional display label alongside locationId. It never identifies a
            tenant; selecting by name alone returns 400.
          example: Roofing Leads
        - name: locationId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: >-
            Location ID of the project to read. Required when selecting a
            project; omit every project selector for an agency-wide read.
            Project names are labels only and never select a project on their
            own.
          example: loc_9f7a123
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: >-
            Restrict the summary to the leads one campaign dialled. Calls
            recorded without campaign attribution are not returned by this
            filter, so an older window can show fewer leads here than an
            unfiltered read.
          example: cmp_9f7a123
        - name: masterCampaignId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: >-
            Restrict the summary to the leads an entire master campaign dialled,
            across every linked project the caller can see. A master campaign is
            fanned out into one child campaign per project, each with its own
            id, so filtering by a single campaignId returns one project's share
            of it. Mutually exclusive with campaignId: the two are contradictory
            scopes rather than narrowing ones, and sending both is a 400.
          example: mc_84f1be18-2329-4af1-a2f0-abcf2170aba9
        - name: agentVariantId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: >-
            Restrict the figures to one Agent Variant inside a split test, so
            two variants of the same campaign can be compared. Requires
            `campaignId` and cannot be combined with `masterCampaignId`: a
            variant id only identifies a variant within its own campaign, so an
            unscoped one has no meaning and is rejected.
          example: agent_8a488fcfa8fdbf8ce8ad5ccc45
      responses:
        '200':
          description: Analytics per lead-age segment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsLeadAgeResponse'
              examples:
                default:
                  value:
                    success: true
                    data:
                      window:
                        from: '2026-07-01'
                        to: '2026-07-31'
                      definitions:
                        freshMaxMinutes: 5
                        dbrMinDays: 3
                      segments:
                        fresh:
                          leads: 180
                          calls: 240
                          pickups: 150
                          conversations: 96
                          inboundCalls: 12
                          outboundCalls: 228
                          appointments: 30
                          transfers: 14
                          failedTransfers: 3
                          attemptedTransfers: 17
                          pickupRate: 62.5
                          conversationRate: 40
                          bookingRate: 16.67
                          transferRate: 9.44
                          appointmentConversionRate: 31.25
                        warm:
                          leads: 110
                          calls: 170
                          pickups: 80
                          conversations: 48
                          inboundCalls: 6
                          outboundCalls: 164
                          appointments: 11
                          transfers: 5
                          failedTransfers: 2
                          attemptedTransfers: 7
                          pickupRate: 47.06
                          conversationRate: 28.24
                          bookingRate: 10
                          transferRate: 6.36
                          appointmentConversionRate: 22.92
                        dbr:
                          leads: 100
                          calls: 160
                          pickups: 52
                          conversations: 28
                          inboundCalls: 4
                          outboundCalls: 156
                          appointments: 5
                          transfers: 2
                          failedTransfers: 1
                          attemptedTransfers: 3
                          pickupRate: 32.5
                          conversationRate: 17.5
                          bookingRate: 5
                          transferRate: 3
                          appointmentConversionRate: 17.86
                        unknown:
                          leads: 30
                          calls: 42
                          pickups: 16
                          conversations: 9
                          inboundCalls: 2
                          outboundCalls: 40
                          appointments: 2
                          transfers: 1
                          failedTransfers: 0
                          attemptedTransfers: 1
                          pickupRate: 38.1
                          conversationRate: 21.43
                          bookingRate: 6.67
                          transferRate: 3.33
                          appointmentConversionRate: 22.22
                      unknownReasons:
                        noLeadRecord: 4
                        noCreationDate: 11
                        noReceiptDate: 0
                        straddlesBoundary: 15
                        createdAfterReceipt: 0
                      totals:
                        leads: 420
                        calls: 612
                        pickups: 298
                        conversations: 181
                        inboundCalls: 24
                        outboundCalls: 588
                        appointments: 48
                        transfers: 22
                        failedTransfers: 6
                        attemptedTransfers: 28
                        pickupRate: 48.69
                        conversationRate: 29.58
                        bookingRate: 11.43
                        transferRate: 6.67
                        appointmentConversionRate: 26.52
                      cached: false
                      queryTime: 1840
                      generatedAt: '2026-07-08T16:00:00.000Z'
        '400':
          description: A query parameter is malformed (e.g. an unparseable date).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: BadRequest
                    message: dateToUTC must be a valid ISO 8601 UTC date-time
        '401':
          description: Missing or invalid authentication.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: Unauthorized
        '403':
          description: The credential is authenticated but not scoped for `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: InsufficientScope
                    message: This API key does not have the required scope.
                    requiredScope: dashboard:read
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: TooManyRequests
        '500':
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: Unexpected server error.
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |-
            curl --request GET \
              --url https://api.teamfollowup.ai/api/dashboard/analytics/lead-age \
              --header 'Authorization: Bearer YOUR_API_KEY'
components:
  schemas:
    AnalyticsLeadAgeResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/AnalyticsLeadAge'
    AnalyticsErrorResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          example: Unauthorized
        message:
          type: string
          example: The request is not permitted.
        requiredScope:
          type: string
          description: On a scope refusal, the scope the credential was missing.
          example: dashboard:read
    AnalyticsLeadAge:
      type: object
      additionalProperties: false
      description: The analytics summary split by lead age.
      required:
        - window
        - definitions
        - segments
        - unknownReasons
        - totals
      properties:
        window:
          type: object
          additionalProperties: false
          description: >-
            The window these results were selected over, resolved from the
            bounds supplied, as inclusive calendar days. Null on a side that was
            left open. Selection works in whole local calendar days, so a bound
            carrying a time is widened to cover the whole of that day. Compare
            against these values rather than the bounds you sent.
          properties:
            from:
              type:
                - string
                - 'null'
              format: date
              description: First day included, or null when unbounded.
              example: '2026-07-01'
            to:
              type:
                - string
                - 'null'
              format: date
              description: Last day included, or null when unbounded.
              example: '2026-07-31'
        definitions:
          type: object
          additionalProperties: false
          required:
            - freshMaxMinutes
            - dbrMinDays
          description: The segment boundaries the figures were computed with.
          properties:
            freshMaxMinutes:
              type: number
              description: >-
                A lead sent within this many minutes of being created is
                `fresh`.
              example: 5
            dbrMinDays:
              type: number
              description: >-
                A lead sent this many days or more after being created is `dbr`.
                Between the two it is `warm`.
              example: 3
        segments:
          type: object
          additionalProperties: false
          required:
            - fresh
            - warm
            - dbr
            - unknown
          properties:
            fresh:
              $ref: '#/components/schemas/AnalyticsLeadAgeSegment'
            warm:
              $ref: '#/components/schemas/AnalyticsLeadAgeSegment'
            dbr:
              $ref: '#/components/schemas/AnalyticsLeadAgeSegment'
            unknown:
              $ref: '#/components/schemas/AnalyticsLeadAgeSegment'
        unknownReasons:
          type: object
          additionalProperties: false
          required:
            - noLeadRecord
            - noCreationDate
            - noReceiptDate
            - straddlesBoundary
            - createdAfterReceipt
          description: Why each lead in `unknown` could not be placed, counted in leads.
          properties:
            noLeadRecord:
              type: integer
              description: The calls have no lead record to read the times from.
            noCreationDate:
              type: integer
              description: The CRM sent no creation date.
            noReceiptDate:
              type: integer
              description: No record of when the lead arrived.
            straddlesBoundary:
              type: integer
              description: >-
                One of the times is only a date, and the range it allows crosses
                a segment boundary.
            createdAfterReceipt:
              type: integer
              description: >-
                The CRM creation time is more than 5 minutes after the lead
                arrived, so the two contradict each other.
        totals:
          $ref: '#/components/schemas/AnalyticsLeadAgeSegment'
        cached:
          type: boolean
          description: Whether the response was served from cache.
        queryTime:
          type: number
          description: Server compute time in milliseconds.
        generatedAt:
          type: string
          format: date-time
          description: When the figures were generated.
    AnalyticsLeadAgeSegment:
      type: object
      additionalProperties: false
      description: >-
        Counts and rates for one lead-age segment, on the same definitions as
        the analytics summary.
      required:
        - leads
        - calls
        - pickups
        - conversations
        - inboundCalls
        - outboundCalls
        - appointments
        - transfers
        - failedTransfers
        - attemptedTransfers
        - pickupRate
        - conversationRate
        - bookingRate
        - transferRate
        - appointmentConversionRate
      properties:
        leads:
          type: integer
          description: Unique leads dialled in the window.
        calls:
          type: integer
          description: Dial attempts.
        pickups:
          type: integer
          description: Calls a human answered.
        conversations:
          type: integer
          description: Answered calls lasting over 30 seconds.
        inboundCalls:
          type: integer
          description: Calls the lead placed.
        outboundCalls:
          type: integer
          description: Calls the AI placed.
        appointments:
          type: integer
          description: Leads who booked an appointment.
        transfers:
          type: integer
          description: Leads successfully transferred live.
        failedTransfers:
          type: integer
          description: Leads whose live transfer failed to connect.
        attemptedTransfers:
          type: integer
          description: Successful plus failed transfers.
        pickupRate:
          type: number
          description: Percentage of calls a human answered.
        conversationRate:
          type: number
          description: Percentage of calls that became a conversation over 30 seconds.
        bookingRate:
          type: number
          description: Appointments as a percentage of leads.
        transferRate:
          type: number
          description: Attempted transfers as a percentage of leads.
        appointmentConversionRate:
          type: number
          description: Appointments as a percentage of conversations.
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: 'Send your API key as `Authorization: Bearer YOUR_API_KEY`.'

````