MeetyyAPI
Documentation / API Reference / Mentor Analytics

Mentor Analytics API

The mentor dashboard (/manage/analytics) in a single request: session counts, delivered hours, the students behind them, the money they produced, and the two breakdowns.

  • Base URL: /api/v1 ยท Auth: Sanctum bearer token ยท Casing: snake_case
  • Related: earnings-api.md (the money surface this shares its definitions with) ยท booking-api.md (the sessions behind the counts) ยท analytics-api-response.md (the reply to the frontend spec, and why four fields differ from it)

GET /mentor/analytics

Own data only. The mentor profile is taken from the token, so there is no id to tamper with. A caller without a mentor profile gets a 404.

Query parameters

Param Type Required Notes
period string no Default last_30_days
compare boolean no Default true

period accepts either vocabulary, because this page also renders /mentor/earnings/summary and forcing one screen to hold two spellings is how bugs get written:

Canonical Short form
last_7_days 7d
last_30_days 30d
last_90_days 90d
last_12_months 12m
all_time all

Anything else is a 422 โ€” a silent fall back to the default would show a mentor the wrong month with no indication it had happened. The response echoes the resolved window in period.value, so a client sending short forms never has to guess what it got.

compare=false skips measuring the previous window, halving the work for a dashboard that polls the figures without redrawing the change indicators. Every change_percent is then null.

Response

{
  "status": "success",
  "message": "Analytics retrieved successfully.",
  "data": {
    "period": {
      "from": "2026-08-09", "to": "2026-09-08",
      "value": "last_30_days", "label": "Last 30 days"
    },
    "bucket": "daily",
    "currency": "USD",

    "stats": {
      "total_sessions":     { "value": 42,   "change_percent": 12.5 },
      "completed_sessions": { "value": 31,   "change_percent": -4.0 },
      "total_hours":        { "value": 25.5, "change_percent": 8.0 },
      "total_students":     { "value": 87,   "change_percent": 5.0 },
      "total_earnings": {
        "value": { "minor": 124000, "major": "1240.00", "formatted": "USD 1,240.00" },
        "change_percent": 22.0
      },
      "average_rating": { "value": null, "change_percent": null },
      "profile_views":  { "value": null, "change_percent": null }
    },

    "sessions_by_status": {
      "requested": 4, "confirmed": 7, "paid": 3,
      "completed": 31, "cancelled": 2, "refunded": 1
    },

    "sessions_over_time": [
      {
        "label": "1 Sep", "date": "2026-09-01",
        "total": 3, "completed": 2, "cancelled": 1,
        "earnings": 34000,
        "earnings_by_currency": { "USD": 34000 }
      }
    ],

    "session_breakdown": {
      "by_provider":      [{ "key": "google_meet", "label": "Google Meet", "count": 18 }],
      "by_session_model": [{ "key": "one_time",    "label": "One Time",    "count": 25 }]
    },

    "earnings": {
      "is_multi_currency": false,
      "currencies": [
        {
          "currency": "USD",
          "total": { "minor": 124000, "major": "1240.00", "formatted": "USD 1,240.00" },
          "change_percent": 22.0
        }
      ]
    }
  }
}

Errors: 401 unauthenticated ยท 404 no mentor profile ยท 422 invalid period


What "in this period" means

A booking is counted in the window its session was scheduled to happen in โ€” the primary slot's starts_at โ€” not when it was booked and not when its status last moved.

That is the only choice under which the numbers on the page agree with one another. A session booked in August for September is September's work; a mentor checking "sessions this month" against their own calendar is comparing like with like; and a cancellation still occupies the slot it was going to occupy, which is what makes a cancellation rate mean anything. Bucketing by status timestamps instead would put a session's completion in a different bucket from the session, and sessions_over_time would stop summing to total_sessions.

Money is the deliberate exception. Earnings are bucketed by when the payment was paid, because that is the definition earnings-api.md already uses, and two surfaces on one screen disagreeing about a mentor's income is worse than one surface using two clocks.

A consequence worth stating plainly: a free session counts in total_sessions and contributes nothing to earnings. The two figures are not meant to reconcile.

Bucketing

period bucket
last_7_days, last_30_days daily
last_90_days weekly
last_12_months monthly
all_time monthly, falling back to yearly past the monthly cap

Buckets are zero-filled: an empty period appears with zeros rather than being absent. A chart with gaps interpolates across them and shows a mentor income in a week they had none.

label is formatted server-side โ€” 12 Mar daily and weekly (the week's opening day), Mar monthly (Mar 2026 when the range crosses a year), 2026 yearly. Every point also carries an ISO date, so you can relabel without parsing.

All-time opens at the mentor's first scheduled session rather than at the epoch, so the chart does not lead with years of empty buckets.

Money

Every figure is net of the platform fee โ€” amount โˆ’ platform_fee โˆ’ refunded โ€” the single definition shared with /mentor/invoices and /mentor/earnings/summary. It counts all three revenue streams: session bookings, course enrolments and community subscriptions. They live in one polymorphic payments table, so including them is free, and an analytics figure that disagreed with the earnings card on the same dashboard would be indistinguishable from a bug.

Two representations, because one cannot be both correct and simple:

  • stats.total_earnings is the headline figure in the mentor's own trading currency (data.currency). Simple, and what most mentors need.
  • earnings.currencies is the complete answer, one block per currency, never summed and never converted โ€” there is no FX rate anywhere in this system. earnings.is_multi_currency tells you when the headline figure is a partial view, and is the signal to render the rows instead of the card.

sessions_over_time[].earnings mirrors the same split: a bare minor-unit integer in the primary currency (the chart only plots it, so a money triple per point would treble the payload), plus earnings_by_currency for the full picture.

change_percent

The change against the immediately preceding window of the same length, measured by the identical code path โ€” two figures computed different ways produce a number that is not a comparison of anything.

It is null, never 0, whenever there is nothing to compare against: compare=false, an all-time window (there is no window before all of history), or a previous window of zero. That last case is an increase from nothing, which has no finite percentage; both "0%" and "100%" would be lies, so the client hides the indicator.

total_hours

Delivered time only, in hours to one decimal. A cancelled session consumed a slot but no hours, and counting it would inflate the one figure a mentor is most likely to check against their own records.

Completed follow-ups count, since they are real delivered sessions with their own slots. They are not counted as separate bookings โ€” a booking with three follow-ups is one booking โ€” so they only ever move this figure.

total_students

Distinct people, not bookings: a mentee who books four sessions counts once. Spans session bookings and course enrolments in the window, de-duplicated on the user, so a mentee who both books and enrols is also one student.

The two null stats

average_rating and profile_views are always null, and are present only so the client can render the same object shape for every card.

  • There is no mentor or session review anywhere in this system. mentor_profiles.average_rating is a column nothing writes, so it is permanently 0.00; serving it would be inventing a rating.
  • Profile views are not tracked at all โ€” no table, no write path.

null is the spec's own signal to hide an indicator, so these cards stay hidden rather than showing a mentor a fabricated zero. If either is built later it will start returning a number in this same shape, with no other change to the response.

Breakdowns

Both are ordered by count, biggest slice first.

  • by_provider comes from the meeting room minted for the slot. A booking with no room yet โ€” a request nobody has confirmed โ€” contributes to total_sessions but to no provider, so this breakdown can legitimately sum to less than the total.
  • by_session_model comes from the session type: regular_weekly, recurring, one_time. (The frontend spec's example showed one_on_one / 1-on-1; no such model exists โ€” capacity is a separate field on the session type.)

A value with no matching enum case still appears, labelled with its raw key, rather than being dropped: it is real volume, and silently omitting it would make the breakdown fail to add up.