跳到主要内容

Notifications, calendar and status

Three groups serve the data behind akn's own sites. Notifications and calendar are gated by the startpage project grant. Status is gated by the admin role or the status grant.

MethodPathCredential
POST/startpage/notifyAPI key
GET/startpage/notificationsSession cookie or bearer ID token, plus the startpage grant
GET/calendar/eventsSession cookie or bearer ID token, plus the startpage grant
GET/calendar/upcoming-locationsNone
GET/statusSession cookie, admin role or status grant
POST/status/incidentSession cookie, admin role or status grant
PATCH/status/incidentSession cookie, admin role or status grant
POST/status/urlSession cookie, admin role or status grant
DELETE/status/urlSession cookie, admin role or status grant

POST /startpage/notify

Submits a notification. Server to server, with no user session.

FieldTypeRequiredDetail
titlestringYes
bodystringYes
sourcestringNoandroid or openclaw. Any other value is stored as other
ConditionStatusBody
Accepted200{"ok":true,"id":123}
Key missing or wrong401{"error":"Unauthorized"}
title or body missing400{"error":"title and body required"}
Store rejected the write500{"error":"…"}

No length limit is enforced and no deduplication is applied. A repeated submission creates a second notification.

GET /startpage/notifications

ParameterInRequiredDetail
sinceQueryNoMilliseconds since the epoch. Returns notifications created after that instant
{ "notifications": [ { "id": 1, "title": "…", "body": "…", "source": "android", "created_at": "…" } ] }
PropertyDetail
OrderingOldest first, with and without since
Limit20 per request
Without sinceThe 20 most recent notifications
With sinceThe 20 oldest notifications after that instant
PaginationNot available. Poll with since set to the newest created_at already held

The feed is not per-caller. Every holder of the startpage grant reads the same notifications.

ConditionStatusBody
Store rejected the read500{"error":"…"}

GET /calendar/events

Returns the owner's calendar for the coming week.

{ "events": [ { "uid": "…", "summary": "…", "location": "…", "joinUrl": "…", "start": "…", "end": "…", "allDay": false } ] }
PropertyDetail
WindowNow to seven days ahead
Limit10 events, earliest first
joinUrlPresent when the event carries a conference link
locationPresent when the event carries one
Recurring eventsExpanded by the upstream calendar within the window

An upstream failure, and a service with no calendar configured, both answer 200 with {"events":[]}. An empty array does not mean the week is free.

GET /calendar/upcoming-locations

Public. Returns places inferred from the calendar over the coming month.

{
"locations": [
{
"city": "Tokyo",
"country": "Japan",
"when": "3 Sep",
"start": "…",
"end": "…",
"tentative": false,
"status": "confirmed",
"confidence": "high",
"confidenceScore": 0.95
}
]
}
PropertyDetail
Window31 days ahead
Limit12 entries, earliest first, one per city per day
statusconfirmed or tentative
confidencehigh, medium or low
confidenceScore0 to 1

Places are inferred from event text against a fixed list of known cities. An event elsewhere produces no entry. Events that read as remote, and events whose location is a URL, are skipped.

A failure answers 200 with {"locations":[]}.

GET /status

Returns the incident record and the list of monitored URLs.

{
"incidents": { "active": [], "resolved": [] },
"urls": [ { "name": "…", "url": "https://…" } ],
"incidentsSha": "…",
"urlsSha": "…"
}

The incidentsSha and urlsSha fields identify the revision each list was read from. They are informational. No endpoint accepts them back.

POST /status/incident

FieldTypeRequired
titlestringYes
servicestringYes
descriptionstringNo
urlstringNo
ConditionStatusBody
Created200{"ok":true,"incident":{…}}
title or service missing400{"error":"title and service required"}

The incident is created with status investigating and added to the top of the active list. Its incidentId is derived from the service name and the creation time.

PATCH /status/incident

FieldTypeRequiredDetail
incidentIdstringYesFrom the active list
actionstringYesresolve or update
updatesobjectConditionalFields to merge. Required in effect for update
ConditionStatusBody
Applied200{"ok":true}
incidentId or action missing400{"error":"incidentId and action required"}
No active incident with that id404{"error":"not found"}
action is neither value400{"error":"unknown action"}

resolve moves the incident to the resolved list, stamps the end time, and drops its status field. A resolved incident cannot be reopened through the API.

update merges updates into the active incident with no field validation. A misspelled key is written as a new field.

POST /status/url

FieldTypeRequired
namestringYes
urlstringYes
ConditionStatusBody
Added200{"ok":true}
A field is missing400{"error":"name and url required"}

Adding a name that already exists appends a second entry rather than replacing the first.

DELETE /status/url

Takes a JSON body, not a query parameter.

FieldTypeRequired
namestringYes
ConditionStatusBody
Removed200{"ok":true}
name missing400{"error":"name required"}

Removing a name that is not present answers 200 and changes nothing.

Status access constraint

The /status endpoints decide access from a legacy account record rather than the person record used everywhere else. An account created after the identity migration can hold the admin role, pass every other gated endpoint, and still be refused here with 403.

Concurrency

Incident and URL changes are read-modify-write against a single stored revision. Two administrators acting at the same time can produce a failed write, or a change that overwrites the other. Reload before each change.