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

# List call detail records

> Newest first. A team_supervisor or agent is restricted to their own team regardless of filters. Sort keys: billsec, caller_number, destination_number, direction, ended_at, sell_amount, started_at. An unknown sort key is a 400.



## OpenAPI

````yaml /api-reference/openapi-public.json get /cdrs
openapi: 3.0.3
info:
  description: >-
    The API your own systems use: place and follow calls, have an AI agent call
    someone, run campaigns and get their results, keep contacts in step with
    your CRM, and receive signed webhooks. Authenticate with an integration key
    (Authorization: Bearer ft_...), used only from the IP addresses it allows.
  title: FireTone API
  version: 0.1.0
servers:
  - description: Your platform's API host
    url: https://{host}/api/v1
    variables:
      host:
        default: api.firet.one
security:
  - bearerAuth: []
tags:
  - name: Auth
  - description: >-
      Live calls and what can be done to them: hang up, hold, transfer, park,
      merge, monitor, whisper; the Desk's own call.
    name: Calls
  - description: 'Outbound campaigns: contacts, attempts, outcomes.'
    name: Campaigns
  - description: 'Customers: who called, what is known about them, and their memory.'
    name: Contacts
  - description: What was said on an AI call, and the review of it.
    name: Conversations
  - description: >-
      Your own systems: HTTP connections an IVR calls mid-call, and webhooks for
      call events. Tenant URLs must be public https addresses.
    name: Integrations
  - name: Live
  - name: Provisioning
  - name: Reporting
  - description: Tickets raised by people, agents and the API.
    name: Tickets
  - description: Messages left for an extension or a queue.
    name: Voicemail
paths:
  /cdrs:
    get:
      tags:
        - Reporting
      summary: List call detail records
      description: >-
        Newest first. A team_supervisor or agent is restricted to their own team
        regardless of filters. Sort keys: billsec, caller_number,
        destination_number, direction, ended_at, sell_amount, started_at. An
        unknown sort key is a 400.
      operationId: getCdrs
      parameters:
        - $ref: '#/components/parameters/filterExtensionId'
        - $ref: '#/components/parameters/filterDialledNumber'
        - description: >-
            Calls placed by one campaign. The column has been on the CDR since
            the campaign tables were created and was neither returned nor
            filterable, so "what did this campaign cost" -- in minutes or in
            tokens -- could not be asked here.
          in: query
          name: campaign_id
          schema:
            format: uuid
            type: string
        - in: query
          name: from
          schema:
            format: date-time
            type: string
        - in: query
          name: to
          schema:
            format: date-time
            type: string
        - in: query
          name: team_id
          schema:
            format: uuid
            type: string
        - in: query
          name: agent_id
          schema:
            format: uuid
            type: string
        - in: query
          name: direction
          schema:
            enum:
              - inbound
              - outbound
              - internal
            type: string
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/organisationId'
        - $ref: '#/components/parameters/sort'
        - $ref: '#/components/parameters/order'
        - $ref: '#/components/parameters/q'
        - $ref: '#/components/parameters/filterFrom'
        - $ref: '#/components/parameters/filterTo'
        - $ref: '#/components/parameters/filterTeamId'
        - $ref: '#/components/parameters/filterAgentId'
        - $ref: '#/components/parameters/filterDirection'
        - $ref: '#/components/parameters/filterTransport'
        - $ref: '#/components/parameters/filterHangupCause'
        - $ref: '#/components/parameters/filterTranscoded'
        - $ref: '#/components/parameters/filterTrunkId'
        - $ref: '#/components/parameters/filterMinBillsec'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CDRPage'
          description: OK
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
components:
  parameters:
    filterExtensionId:
      description: Filter on the extension the call was on.
      in: query
      name: extension_id
      schema:
        format: uuid
        type: string
    filterDialledNumber:
      description: >-
        Filter on the number dialled, matched as a case-insensitive SUBSTRING:
        "7231" finds every call to a number ending 7231. A % or _ in the value
        is matched literally, not as a wildcard. For an inbound call the number
        dialled is the DID it arrived on; it also matches an outbound call to
        that number, so pair it with direction=inbound for arrivals only.
      in: query
      name: did
      schema:
        type: string
    limit:
      in: query
      name: limit
      schema:
        default: 50
        maximum: 500
        type: integer
    offset:
      in: query
      name: offset
      schema:
        default: 0
        type: integer
    organisationId:
      description: Narrow to one organisation. Outside your scope returns 403.
      in: query
      name: organisation_id
      schema:
        format: uuid
        type: string
    sort:
      description: >-
        Sort key. Each list accepts its own allow-list of keys, given in that
        endpoint's description; anything else is a 400. ORDER BY cannot be
        parameterised, so the key is looked up rather than interpolated. Every
        sort carries a tiebreak on the primary key, so paging stays disjoint
        when the sort column has duplicates.
      in: query
      name: sort
      schema:
        type: string
    order:
      description: >-
        Sort direction. Defaults to ascending, except the call log, which reads
        newest first.
      in: query
      name: order
      schema:
        enum:
          - asc
          - desc
        type: string
    q:
      description: >-
        Free-text search across the list's searchable columns (case-insensitive
        substring). % and _ in the term match themselves.
      in: query
      name: q
      schema:
        type: string
    filterFrom:
      description: Inclusive lower bound on started_at. RFC3339 timestamp or YYYY-MM-DD.
      in: query
      name: from
      schema:
        type: string
    filterTo:
      description: Exclusive upper bound on started_at. RFC3339 timestamp or YYYY-MM-DD.
      in: query
      name: to
      schema:
        type: string
    filterTeamId:
      description: Filter on team.
      in: query
      name: team_id
      schema:
        format: uuid
        type: string
    filterAgentId:
      description: Filter on agent.
      in: query
      name: agent_id
      schema:
        format: uuid
        type: string
    filterDirection:
      description: 'Filter on direction: inbound or outbound.'
      in: query
      name: direction
      schema:
        type: string
    filterTransport:
      description: Filter on transport.
      in: query
      name: transport
      schema:
        type: string
    filterHangupCause:
      description: Filter on hangup cause.
      in: query
      name: hangup_cause
      schema:
        type: string
    filterTranscoded:
      description: Only transcoded calls.
      in: query
      name: transcoded
      schema:
        type: boolean
    filterTrunkId:
      description: Filter on trunk.
      in: query
      name: trunk_id
      schema:
        format: uuid
        type: string
    filterMinBillsec:
      description: Only calls billed at least this many seconds.
      in: query
      name: min_billsec
      schema:
        type: integer
  schemas:
    CDRPage:
      properties:
        items:
          items:
            $ref: '#/components/schemas/CDR'
          type: array
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
      required:
        - items
        - total
        - limit
        - offset
      type: object
    CDR:
      properties:
        agent_id:
          format: uuid
          nullable: true
          type: string
        ai_tokens:
          description: >-
            Input plus output tokens for the AI that handled this call; absent
            when no AI did. Read from the conversation rather than the charge,
            because a charge row exists only where settlement found something to
            price -- sourcing it from the charge would report "no AI" for a call
            an AI demonstrably answered. Audio is not added: it is the audio
            portion of the input, and adding it double-counts.
          format: int64
          type: integer
        answered_at:
          format: date-time
          nullable: true
          type: string
        billing_mode:
          enum:
            - rated
            - free
          type: string
        billsec:
          type: integer
        bridge_cause:
          description: >-
            The far end's hangup cause for the last bridge attempt, e.g. a
            carrier's CALL_REJECTED. hangup_cause is this leg's own. Absent when
            there was no bridge or no answer.
          type: string
        call_uuid:
          format: uuid
          type: string
        caller_number:
          type: string
        campaign_id:
          description: The campaign that placed this call, when one did.
          format: uuid
          type: string
        codec_a:
          nullable: true
          type: string
        codec_b:
          nullable: true
          type: string
        contact_id:
          description: The customer this call was with.
          format: uuid
          type: string
        cost_amount:
          description: >-
            Exact decimal amount as a string. Never a JSON number: rates are
            fractions of a cent and binary floating point cannot represent them
            exactly.
          example: '0.062000'
          type: string
        cost_amount_original:
          description: >-
            What the carrier charged, in cost_currency, before conversion.
            cost_amount is this multiplied by fx_rate. Omitted without
            margin:read.
          example: '0.024000'
          type: string
        cost_currency:
          description: The currency the CARRIER charged in. Omitted without margin:read.
          type: string
        cost_unconverted:
          description: >-
            True when no exchange rate existed for the pair: cost_amount is then
            the carrier's RAW figure and margin is a subtraction across two
            currencies, which means nothing. Present only when true, and only
            with margin:read.
          type: boolean
        currency:
          description: >-
            ISO 4217 code that sell_amount, cost_amount and margin are all in:
            the call's own currency, which is its sell rate's. Absent on calls
            recorded before it was written.
          type: string
        destination_number:
          type: string
        dialled_number:
          description: >-
            What the caller keyed in, when routing read it as a different number
            (919216217231 routed as +919216217231). Absent when it was dialled
            as routed.
          type: string
        direction:
          enum:
            - inbound
            - outbound
            - internal
          type: string
        ended_at:
          format: date-time
          nullable: true
          type: string
        fx_rate:
          description: >-
            The exchange rate in force when the call was settled: one unit of
            cost_currency in currency. Stored on the call, so a margin does not
            move when a later rate is entered. Omitted without margin:read.
          example: '83.20000000'
          type: string
        hangup_cause:
          nullable: true
          type: string
        jitter_ms:
          description: >-
            Peak inbound jitter in milliseconds. If every leg of every call
            shows it at once, the cause is the node being scheduled late rather
            than any one network.
          nullable: true
          type: number
        loss_pct:
          description: >-
            Percent of inbound packets that never arrived. ABSENT, not zero,
            when too little audio arrived to judge: FreeSWITCH scores a leg that
            received nothing as quality 100 and MOS 4.50, and zero is a
            measurement this leg does not have.
          nullable: true
          type: number
        margin:
          description: >-
            Exact decimal amount as a string. Never a JSON number: rates are
            fractions of a cent and binary floating point cannot represent them
            exactly.
          example: '0.062000'
          type: string
        mean_interval_ms:
          description: >-
            Mean gap between arriving packets. About 20 on a healthy 20 ms
            stream; double the negotiated ptime is a ptime mismatch.
          nullable: true
          type: number
        media_verdict:
          description: >-
            What the RTP layer said about the audio that ARRIVED on the caller's
            leg. The codec and the duration both look healthy on a call nobody
            could understand; this does not. lossy_inbound and jittery_inbound
            are statistics of what reached the switch, so the damage was done
            before it got here. Absent for a leg with no RTP session.
          enum:
            - ok
            - unanswered
            - no_inbound_media
            - no_outbound_media
            - wrong_advertised_address
            - lossy_inbound
            - jittery_inbound
            - unpaced_sender
            - ptime_mismatch
            - degraded_audio
          nullable: true
          type: string
        mos:
          description: >-
            Derived from quality_pct alone, so it is the same measurement on a
            different scale and not an independent opinion.
          nullable: true
          type: number
        quality_pct:
          description: FreeSWITCH's own (received - flaws) / received.
          nullable: true
          type: number
        sell_amount:
          description: >-
            Exact decimal amount as a string. Never a JSON number: rates are
            fractions of a cent and binary floating point cannot represent them
            exactly.
          example: '0.062000'
          type: string
        sip_status:
          description: >-
            The far end's final SIP response for the last bridge attempt: 403,
            503, 486. Absent when none was received.
          type: integer
        started_at:
          format: date-time
          type: string
        team_id:
          format: uuid
          nullable: true
          type: string
        transcoded:
          description: >-
            codec_a differs from codec_b. A rising transcode ratio is the
            leading indicator of a capacity problem: it presents as 'the switch
            feels slow' long before anything errors.
          type: boolean
        transport:
          enum:
            - sip
            - whatsapp
          type: string
      required:
        - call_uuid
        - direction
        - transport
        - started_at
        - billsec
        - transcoded
        - billing_mode
      type: object
    Error:
      properties:
        error:
          properties:
            code:
              enum:
                - invalid_request
                - invalid_credentials
                - unauthenticated
                - forbidden
                - not_found
                - conflict
                - internal
              type: string
            message:
              type: string
          required:
            - code
            - message
          type: object
      required:
        - error
      type: object
  responses:
    BadRequest:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
      description: The request is not valid. The message names every field that is wrong.
    Unauthorized:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
      description: Missing, invalid or expired token
    Forbidden:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
      description: Outside the caller's scope, or insufficient role
  securitySchemes:
    bearerAuth:
      bearerFormat: JWT or API key
      description: >-
        Every request sends `Authorization: Bearer <token>`. The token is either
        a panel session (a JWT from /auth/login, 12 hours) or an API key
        `ft_<id>_<secret>`. An API key is accepted only from an address on its
        IP allowlist (403 ip_not_allowed otherwise; 403 ip_allowlist_required
        for an old key that has none), is limited to its rate per minute (429
        rate_limited with Retry-After; X-RateLimit-Limit/Remaining/Reset on
        every response), and at most 60 call placements a minute.
      scheme: bearer
      type: http

````