Skip to main content

Screentime

This guide covers integrating screentime: reporting when a player is using your product, reading the verdict, handling the seven screentime webhook events, and letting a player ask a parent for more time.

For what screentime is and where the boundary between k-ID and your product sits, read Screentime concepts first.

Before you start

  • Check that screentime is available to your organization. Please contact k-ID to have this capability enabled for your organization.
  • Enable the feature for your product. Screentime is off by default and takes two switches in Compliance Studio, in this order: turn on Screentime, then Screentime Controls, then publish the change. Controls depends on Screentime and stays unavailable until Screentime is on. Until the switch a given endpoint needs is on, that endpoint fails with FEATURE_DISABLED, and the two groups of endpoints need different switches. See Feature flags.
  • Configure your webhook endpoint. All seven screentime signals arrive as webhooks. Set the URL and secret under Developer Settings for the product, and validate signatures as described in the webhooks overview.
  • Subscribe to the events you handle. An endpoint receives only the event types it's subscribed to, and k-ID drops the rest without a delivery attempt or an error. On the product's Developer Settings page in Compliance Studio, select Screentime.BreakReminder, Screentime.LimitWarning, Screentime.LimitReached, Screentime.ScheduleChanged, Screentime.QuietHoursWarning, Screentime.QuietHoursReached, and Screentime.OverrideResult for your endpoint.
  • Have a session for a player who has a kuid. Every screentime call is addressed to a k-ID sessionId, but the schedule behind it lives against the player's kuid rather than against the session. A session with no kuid can never have a schedule, so GET /screentime/get-state returns { "enabled": false } for it however the parent configures things. A player gets a kuid once a trusted adult has completed consent for them, so run that flow first and replay the stored kuid on later sessions. See Sessions and permissions and Challenges.

A player with no parent-configured schedule isn't an error. GET /screentime/get-state returns { "enabled": false }, no verdict ever blocks, and no events fire. Build for that case: most of your players are in it.

Reporting usage

There are two ways to report usage, and the choice isn't cosmetic. It determines whether your product gets real-time signals at all.

Live session: start and endAfter the fact: push
Endpoints/screentime/start, /screentime/end/screentime/push
Counts toward the daily totalyesyes
Screentime.BreakReminderyesno
Screentime.LimitWarningyesno
Screentime.LimitReachedyesno
Reporting windowreal time, within 5 minutes of the eventany time in the last 7 days
Use it whenyour product knows when a player starts and stopsyour product only learns usage after a session ends
Pick start and end if you need the real-time events

This is the part of screentime that isn't guessable from the endpoint list. Usage reported through /screentime/push is added to the daily total and shows up in a parent's Family Connect charts, but it fires no break reminder, no limit warning, and no limit-reached event.

The reason is timing. k-ID schedules the reminders and warnings when you call /screentime/start, so they fire while the player is still in the session. A 90-minute session reported after the fact could only produce a 60-minute break reminder 30 minutes late, which isn't a break reminder. If your product carries break-reminder obligations, /screentime/push can't satisfy them.

Quiet-hours events and Screentime.ScheduleChanged are driven by the parent's calendar rather than by your session reporting, so they fire either way.

The session state machine

Only one screentime session is active per sessionId at a time, and these rules follow from that:

  • You generate the id. Send a fresh UUID with /screentime/start and send the same id to /screentime/end. An end whose id doesn't match the active session returns NOT_FOUND.
  • Retries are safe. Repeating a start with the same id returns accepted without double-counting. Repeating an end for a session that already ended also returns accepted.
  • A second start with a different id returns status: "replaced". k-ID ends the previous session, counts its duration, and makes the new one active. This is your recovery path when an end never arrived because the app was killed or the device restarted: just call start again and check for replaced. It's also what happens when the same player starts playing on a second device, and there it has a consequence a parent can see: the first device stops counting and stops receiving reminders. See How usage is counted.
  • k-ID ends a stale session after 4 hours. The duration up to that point is counted and the pending reminders are canceled. This bounds the damage when a product stops sending end entirely. It's a backstop, not a substitute for calling end.
  • Timestamps must be real time. /screentime/start rejects a timestamp more than 5 minutes in the past or any time in the future. /screentime/end requires a timestamp at or after the start's, and tolerates about a minute of forward clock skew.

Both start and end return the counter in their response, so you don't need a separate GET /screentime/get-state call just to show a player their remaining time at the top or bottom of a session:

{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"id": "3f8c1d24-6b5e-4a07-9c2f-8d1e7a4b6c90",
"status": "accepted",
"screentime": {
"counter": {
"timeUsedTodayMinutes": 45,
"timeLimitTodayMinutes": 120,
"timeRemainingTodayMinutes": 75,
"dayResetsAt": "2026-06-25T00:00:00Z"
}
}
}

Reporting after the fact

/screentime/push takes up to 100 completed segments in one call. Each segment carries its own id for idempotency, the timestamp at which the span of usage began, and its durationSeconds.

Segments are processed independently, so read the per-event statuses rather than assuming the whole batch succeeded:

{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"accepted": 1,
"rejected": 1,
"events": [
{ "id": "9d4e2f81-7a3b-4c56-8e90-1f2a3b4c5d6e", "status": "accepted" },
{ "id": "c1b2a3d4-5e6f-4708-9a1b-2c3d4e5f6071", "status": "rejected", "reason": "invalid_input" }
]
}

A reason of invalid_input means the segment broke a validation rule and retrying it unchanged fails again: the segment must already be complete, its timestamp must be within the last 7 days, and durationSeconds must be between 1 second and 24 hours. A reason of internal_error is transient and safe to retry. Resubmitting an already-accepted id counts as accepted again without double-counting.

Reading the state

GET /screentime/get-state is the single source of truth for the rules and the current verdict. Call it when a session starts, and again after any event that changes the picture.

{
"enabled": true,
"access": {
"allowed": false,
"details": {
"reason": "quiet_hours",
"resumesAt": "2026-06-25T07:00:00+09:00"
}
},
"state": {
"timeUsedTodayMinutes": 75,
"continuousUsageMinutes": 30,
"timeLimitTodayMinutes": 120,
"timeRemainingTodayMinutes": 45,
"dayResetsAt": "2026-06-25T00:00:00Z"
},
"schedule": {
"timezone": "Asia/Tokyo",
"breakReminderIntervalMinutes": 45,
"dailyLimits": [{ "day": "mon", "limitMinutes": 120 }],
"quietHours": [
{
"name": "Bedtime",
"days": ["mon", "tue", "wed", "thu", "fri"],
"start": "21:00",
"end": "07:00"
}
]
}
}

Read it in this order:

  1. enabled. When it's false the response has nothing else in it, and there are three reasons for that: the parent hasn't configured screentime, the parent configured it and turned enforcement off, or the session has no kuid, so there is no player for a schedule to belong to. In testing the third is the one you're most likely to hit. Don't restrict anything in any of the three cases.
  2. access.allowed. This is the verdict, already combining the daily limit and every quiet-hours window. Don't recompute it from schedule yourself.
  3. access.details. Present only when allowed is false. reason is quiet_hours or limit_reached, and resumesAt is when the restriction lifts, which is the useful thing to tell a player. When both restrictions apply at once, the response reports whichever ends later.
  4. state. Today's numbers for your own UI. timeUsedTodayMinutes includes the time in the session currently in flight, and continuousUsageMinutes is the length of that session alone. timeLimitTodayMinutes and timeRemainingTodayMinutes are absent when no limit is set for today's weekday.
  5. schedule. The parent's rules, for display. Times are HH:MM in schedule.timezone.

Handling the events

Each of these reaches you only if the product's webhook is subscribed to that event type, so check Before you start before debugging a handler that never runs.

EventWhat it meansWhat to do
Screentime.BreakReminderThe player has been in one session for the configured intervalPrompt the player to take a break
Screentime.LimitWarning15 or 5 minutes left todayTell the player, so they can reach a save point
Screentime.LimitReachedToday's limit appliesApply your restriction, and offer to ask a parent
Screentime.QuietHoursWarningA window starts in 15 minutesTell the player which window and when
Screentime.QuietHoursReachedA window is now openApply your restriction until endsAt
Screentime.ScheduleChangedA parent changed the rulesFetch get-state again and replace your copy
Screentime.OverrideResultA parent answered an override requestApply the state in the payload

Three properties of the delivery are worth designing for:

  • Events are addressed to a sessionId. Route on data.sessionId, which is present on all seven.
  • Delivery is at-least-once. The same event can arrive twice. Make your handler idempotent, as described in the webhooks overview.
  • Quiet-hours events don't need an active session. They fire on the parent's calendar for any player with a live k-ID session on your product, whether or not they're playing at the time. Don't assume there's a UI to interrupt.

Asking a parent for more time

When access.allowed is false, offer the player a way to ask rather than only a wall.

  1. Call /screentime/request-override with the sessionId. The response carries an id and an expiresAt. Store the id.
  2. k-ID notifies the parent, who grants or denies the request in Family Connect.
  3. The outcome arrives as Screentime.OverrideResult, correlated by data.id, carrying status of granted, denied, or expired.
  4. Read data.state, which is the full get-state shape computed after the decision. No follow-up fetch is needed.

The endpoint is idempotent per player: calling it again while a request is still pending returns the pending request unchanged instead of creating a second one, and the response shape is identical either way, so you don't need a separate code path for it. An unanswered request expires after 24 hours and fires the webhook with status: "expired". The player can then ask again.

A granted override raises today's limit. It doesn't disable the schedule, and it doesn't extend to tomorrow.

Timezones and day boundaries

Every schedule carries a time zone, and every screentime rule is evaluated in it. Send timestamps in RFC 3339 with an offset and let k-ID handle the conversion.

  • The daily limit resets at local midnight in the schedule's time zone. dayResetsAt in the state response is that instant.
  • A session that crosses local midnight is split on /screentime/end, so each day's total reflects the usage that happened on that day. The same is true of a pushed segment that spans midnight.

Feature flags

Screentime has two tiers, and a product can enable just the first:

FlagWhat it enablesEndpoints it gates
ScreentimePassive usage reporting, no parent setup required/screentime/push
Screentime ControlsParent-configured limits, schedules, quiet hours, break reminders, and overrides/screentime/start, /screentime/end, /screentime/get-state, /screentime/request-override

Screentime on its own lets your product report usage and nothing more: call /screentime/push with completed segments and they count toward the child's daily total, the same shape of integration as activity ingestion. No parent has to set anything up for this to work.

Screentime Controls is the second tier: the parent-configured schedule and everything that acts on it. It gates the session state machine (/screentime/start, /screentime/end), the current verdict (/screentime/get-state), and the ask-a-parent flow (/screentime/request-override), and the real-time events are produced by that session machine.

An endpoint whose tier is off returns FEATURE_DISABLED. A product with only Screentime enabled can push usage but can't start a session or read state; a product with neither can call none of them.

Prelaunch checklist

  • GET /screentime/get-state is called at the start of every session, and enabled: false leaves the player unrestricted.
  • Your product reads access.allowed rather than recomputing the verdict from schedule.
  • Every /screentime/start is paired with an /screentime/end carrying the same id, and a status of replaced is handled rather than treated as an error.
  • Both events in each warning pair are handled, not just the second one.
  • The product's webhook is subscribed to each of the seven Screentime.* event types you handle. An event type the webhook hasn't selected is never delivered.
  • The webhook handler is idempotent and validates the signature.
  • Screentime.ScheduleChanged triggers a fresh fetch of get-state.
  • A player who reaches a limit or a quiet-hours window is offered /screentime/request-override, and all three override statuses are handled, including expired.
  • Restrictions your product applies tell the player when they lift, using resumesAt or endsAt.

Next steps