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, andScreentime.OverrideResultfor your endpoint. - Have a session for a player who has a
kuid. Every screentime call is addressed to a k-IDsessionId, but the schedule behind it lives against the player'skuidrather than against the session. A session with nokuidcan never have a schedule, soGET /screentime/get-statereturns{ "enabled": false }for it however the parent configures things. A player gets akuidonce a trusted adult has completed consent for them, so run that flow first and replay the storedkuidon 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 end | After the fact: push | |
|---|---|---|
| Endpoints | /screentime/start, /screentime/end | /screentime/push |
| Counts toward the daily total | yes | yes |
Screentime.BreakReminder | yes | no |
Screentime.LimitWarning | yes | no |
Screentime.LimitReached | yes | no |
| Reporting window | real time, within 5 minutes of the event | any time in the last 7 days |
| Use it when | your product knows when a player starts and stops | your product only learns usage after a session ends |
start and end if you need the real-time eventsThis 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/startand send the sameidto/screentime/end. Anendwhoseiddoesn't match the active session returnsNOT_FOUND. - Retries are safe. Repeating a
startwith the sameidreturnsacceptedwithout double-counting. Repeating anendfor a session that already ended also returnsaccepted. - A second
startwith a differentidreturnsstatus: "replaced". k-ID ends the previous session, counts its duration, and makes the new one active. This is your recovery path when anendnever arrived because the app was killed or the device restarted: just callstartagain and check forreplaced. 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
endentirely. It's a backstop, not a substitute for callingend. - Timestamps must be real time.
/screentime/startrejects atimestampmore than 5 minutes in the past or any time in the future./screentime/endrequires 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:
enabled. When it'sfalsethe 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 nokuid, 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.access.allowed. This is the verdict, already combining the daily limit and every quiet-hours window. Don't recompute it fromscheduleyourself.access.details. Present only whenallowedisfalse.reasonisquiet_hoursorlimit_reached, andresumesAtis 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.state. Today's numbers for your own UI.timeUsedTodayMinutesincludes the time in the session currently in flight, andcontinuousUsageMinutesis the length of that session alone.timeLimitTodayMinutesandtimeRemainingTodayMinutesare absent when no limit is set for today's weekday.schedule. The parent's rules, for display. Times areHH:MMinschedule.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.
| Event | What it means | What to do |
|---|---|---|
Screentime.BreakReminder | The player has been in one session for the configured interval | Prompt the player to take a break |
Screentime.LimitWarning | 15 or 5 minutes left today | Tell the player, so they can reach a save point |
Screentime.LimitReached | Today's limit applies | Apply your restriction, and offer to ask a parent |
Screentime.QuietHoursWarning | A window starts in 15 minutes | Tell the player which window and when |
Screentime.QuietHoursReached | A window is now open | Apply your restriction until endsAt |
Screentime.ScheduleChanged | A parent changed the rules | Fetch get-state again and replace your copy |
Screentime.OverrideResult | A parent answered an override request | Apply the state in the payload |
Three properties of the delivery are worth designing for:
- Events are addressed to a
sessionId. Route ondata.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.
- Call
/screentime/request-overridewith thesessionId. The response carries anidand anexpiresAt. Store theid. - k-ID notifies the parent, who grants or denies the request in Family Connect.
- The outcome arrives as
Screentime.OverrideResult, correlated bydata.id, carryingstatusofgranted,denied, orexpired. - Read
data.state, which is the fullget-stateshape 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.
dayResetsAtin 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:
| Flag | What it enables | Endpoints it gates |
|---|---|---|
| Screentime | Passive usage reporting, no parent setup required | /screentime/push |
| Screentime Controls | Parent-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-stateis called at the start of every session, andenabled: falseleaves the player unrestricted.- Your product reads
access.allowedrather than recomputing the verdict fromschedule. - Every
/screentime/startis paired with an/screentime/endcarrying the sameid, and astatusofreplacedis 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.ScheduleChangedtriggers a fresh fetch ofget-state.- A player who reaches a limit or a quiet-hours window is offered
/screentime/request-override, and all three override statuses are handled, includingexpired. - Restrictions your product applies tell the player when they lift, using
resumesAtorendsAt.