# OpenAPI spec for the Okilou public API v1. Served at okilou.fr/api/openapi.yaml # and published in the okilou/okilou-homeassistant repository. openapi: 3.1.0 info: title: Okilou API v1 version: "1.0" description: | **Read-only** public API for Okilou (https://okilou.fr). One token covers **the household** (all its children). Tokens are created in the app (Settings → Your household → API & home automation) and can be revoked at any time. The API only exposes what the parent already sees in the app, nothing more. **Shared custody (independent mode)**: a household token only serves that household's days. On the other home's days, `/today` responds with a calm `{ date, isMyDay: false }`, never a 403, never the other household's data. **Latency**: the app syncs within 4 s (push) / 60 s (pull), a fact shows up on the API within 30 to 60 s. **Rate limit**: 30 requests/min per token. contact: name: Okilou url: https://okilou.fr/api/ servers: - url: https://api.okilou.com/functions/v1/api description: Production - url: https://wpicwejpjrpmjkprttmd.supabase.co/functions/v1/api description: Production (original URL, still valid) security: - bearerAuth: [] paths: /children: get: operationId: listChildren summary: Children covered by the token (the household) responses: "200": description: List of children. content: application/json: schema: type: array items: $ref: "#/components/schemas/Child" "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /children/{childId}/today: get: operationId: getToday summary: Today's state computed by the engine (gauge, points, session) parameters: - $ref: "#/components/parameters/childId" responses: "200": description: > Today's state, or a calm `{ date, isMyDay: false }` when the day belongs to the other household (independent shared custody). content: application/json: schema: oneOf: - $ref: "#/components/schemas/TodayPayload" - $ref: "#/components/schemas/CalmDay" "401": { $ref: "#/components/responses/Unauthorized" } "403": description: Child not covered by this token. content: application/json: schema: { $ref: "#/components/schemas/Error" } "409": description: No rules synced for this child yet. content: application/json: schema: { $ref: "#/components/schemas/Error" } "429": { $ref: "#/components/responses/RateLimited" } /children/{childId}/events: get: operationId: listEvents summary: Journal facts (automation triggers) description: | Returns at most 100 events per page, oldest first. Pass the returned `cursor` back as `since` to only receive new events. The cursor is **composite** (`recorded_at,id`) and opaque: store it and pass it back as is. Served types: validations, session starts, one-off rewards, cheers. parameters: - $ref: "#/components/parameters/childId" - name: since in: query required: false schema: type: string description: > The `cursor` from the previous page (or an ISO 8601 timestamp to start from a given instant). Invalid → 400. responses: "200": description: Page of events + cursor. content: application/json: schema: { $ref: "#/components/schemas/EventsPage" } "400": description: Malformed `since` parameter. content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": description: Child not covered by this token. content: application/json: schema: { $ref: "#/components/schemas/Error" } "429": { $ref: "#/components/responses/RateLimited" } components: securitySchemes: bearerAuth: type: http scheme: bearer description: "Household token `rf_api_` created in the Okilou app." parameters: childId: name: childId in: path required: true schema: type: string format: uuid responses: Unauthorized: description: Missing, unknown or revoked token. content: application/json: schema: { $ref: "#/components/schemas/Error" } RateLimited: description: Too many requests (max 30/min per token). content: application/json: schema: { $ref: "#/components/schemas/Error" } schemas: Error: type: object properties: error: { type: string } required: [error] Child: type: object properties: id: { type: string, format: uuid } firstName: { type: string } required: [id, firstName] CalmDay: type: object description: Day at the other household (independent custody), no data. properties: date: { type: string, format: date } isMyDay: { type: boolean, const: false } required: [date, isMyDay] TodayPayload: type: object properties: date: { type: string, format: date } isMyDay: { type: boolean, const: true } period: type: string enum: [school, holiday, weekend] description: Effective period for the day (school zone snapshotted on the token). points: { type: integer } maxPoints: { type: integer } minutesUnlocked: { type: integer, description: "Minutes earned (per day or per week depending on the family's rhythm)." } minutesConsumed: { type: integer } minutesRemaining: { type: integer } consumableToday: { type: boolean, description: "Is consumption allowed today?" } mandatoryOk: { type: boolean, description: "Are today's essentials done?" } perfect: { type: boolean } frozen: { type: boolean } joker: { type: boolean } nextTier: type: [object, "null"] properties: pointsNeeded: { type: integer } unitsGain: { type: integer } behaviors: type: array items: type: object properties: id: { type: string } label: { type: string, description: "Label chosen by the family (user data, never translated)." } status: { type: string, enum: [todo, declared, validated, rejected] } mandatory: { type: boolean } required: [id, label, status, mandatory] session: type: [object, "null"] description: "Reward session. `endsAt` is known as soon as the session starts, `minutes` is the day's cumulative started time." properties: running: { type: boolean } endsAt: { type: string, format: date-time } minutes: { type: integer } streakDays: { type: integer } required: [date, isMyDay, period, points, maxPoints, minutesUnlocked, minutesConsumed, minutesRemaining, consumableToday, mandatoryOk, perfect, frozen, joker, behaviors, streakDays] EventsPage: type: object properties: events: type: array items: { $ref: "#/components/schemas/ApiEvent" } cursor: type: [string, "null"] description: "Pass back as `since` (opaque, composite `recorded_at,id`)." required: [events, cursor] ApiEvent: oneOf: - $ref: "#/components/schemas/BehaviorValidated" - $ref: "#/components/schemas/SessionStarted" - $ref: "#/components/schemas/MenuItemConsumed" - $ref: "#/components/schemas/CheerSent" discriminator: propertyName: type EventBase: type: object properties: id: { type: string, format: uuid } at: { type: string, format: date-time, description: "Server timestamp (recorded_at)." } day: { type: string, format: date } childId: { type: string, format: uuid } required: [id, at, day, childId] BehaviorValidated: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: { type: string, const: behavior_validated } behaviorId: { type: [string, "null"] } label: { type: [string, "null"] } required: [type] SessionStarted: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: { type: string, const: session_started } minutes: { type: integer, description: "0 if unknown." } required: [type, minutes] MenuItemConsumed: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: { type: string, const: menu_item_consumed } menuItemId: { type: [string, "null"] } label: { type: [string, "null"] } required: [type] CheerSent: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: { type: string, const: cheer_sent } cheer: { type: string, enum: [bravo, super, seen] } required: [type, cheer]