# Ordentus — AI Agent / API Skills Guide This document tells an AI agent (or any external script) how to call the Ordentus Calendar API on behalf of a single user. It is the canonical reference: paste the URL `https://ordent.us/llms-api.txt` into your agent's system prompt or knowledge base. --- ## 1. Authentication Every request must include an `Authorization: Bearer ` header. Tokens are minted from **Account Settings → API Access** inside the web app, and look like: ordt_abcdefghij... (around 40-50 chars total) There are two scopes, chosen at token creation: | Scope | Allows | | -------- | ---------------------------------------------------------------------------- | | `read` | All `GET` requests | | `write` | All `POST`, `PATCH`, `PUT`, `DELETE` requests | | `health` | Additionally unlocks `/api/health/*` (sleep, bedtime, cognition, workouts). Off by default. | A token can hold any combination. `read`/`write` are enforced by HTTP method. `health` is an orthogonal opt-in: without it, every `/api/health/*` request returns `403` (health data is sensitive, so it's off unless you tick it when creating the token). A health request still also needs the matching method scope — e.g. updating your sleep target needs `write` **and** `health`. Check `auth.can_access_health` from `whoami` before calling any health endpoint. Quick sanity check: curl -H "Authorization: Bearer $ORDENTUS_TOKEN" \ https://ordent.us/api/auth/me A `200 OK` with `{"id": ..., "username": ...}` means the token works. --- ## 2. Base URL https://ordent.us Everything below is relative to that origin. Responses are JSON unless the path ends in `.ics`. --- ## 2.5 OpenAPI schemas (for ChatGPT custom GPTs / Action builders) Machine-readable OpenAPI 3.1 specs are published for agents that connect via an Action. **ChatGPT caps each Action at 30 operations**, so the surface is split across two schemas — import BOTH as separate Actions on the same GPT (they share the same bearer-token auth): | Schema URL | Covers | | ------------------------------------------------ | ------------------------------------------------------------- | | `https://ordent.us/ordentus-openapi.yaml` | CORE — calendar/events, tasks & store, feeds, reminders (23 ops). | | `https://ordent.us/ordentus-openapi-extras.yaml` | EXTRAS — share links, settings, hidden events (12 ops). | | `https://ordent.us/ordentus-openapi-health.yaml` | HEALTH — sleep, bedtime, cognition, workouts (18 ops). Add only if your token has the `health` permission. | If you only need calendar + tasks, the core schema alone is enough. Each spec lists `whoami` so every Action can self-check its token's scope. --- ## 3. The endpoints an agent will use most ### 3.1 Identity | Method | Path | Purpose | | ------ | ---------------- | ------------------------------------ | | GET | `/api/auth/me` | Returns the current user record. | ### 3.2 Calendar events | Method | Path | Purpose | | ------ | ----------------------------- | -------------------------------------------------------------------------------- | | GET | `/api/events` | List events. Params: `start`, `end` (ISO), `expand=true`, `feed_id`, `include_hidden`, `limit`. | | POST | `/api/events` | Create an event. Body: `{summary, dtstart, dtend, all_day, location, calendar, ...}`. | | GET | `/api/events/trash` | List soft-deleted (trashed) events. Recoverable for 30 days. | | GET | `/api/events/{id}` | Single event. | | PATCH | `/api/events/{id}` | Update fields on an event. Marks it user-modified so sync won't overwrite it. | | DELETE | `/api/events/{id}` | Soft-delete to Trash (default). Pass `?permanent=true` to hard-delete. | | POST | `/api/events/{id}/restore` | Restore a trashed event back to the live calendar. | | GET | `/api/events/{id}/export.ics` | Single-event `.ics` download. | Date times: ISO 8601, e.g. `2026-06-01T09:00:00+10:00`. `all_day` events use `DATE` (`2026-06-01`) for `dtstart`/`dtend`. **Targeting a calendar.** When the user names a calendar ("add it to my Personal calendar"), pass `calendar: "Personal"` on create — it's resolved by name to that manual calendar. Omitting it drops the event in the default "Manual Events" calendar. Unknown names return 400 listing the valid calendars (you can also call `listFeeds` to see them, or pass `feed_id` directly). To move an existing event, `PATCH /api/events/{id}` with `calendar` (or `feed_id`). Only manual calendars accept events — subscription feeds (Google, uni timetables) are read-only. **Recurring events:** pass `expand=true` together with `start` and `end` to receive individual occurrences instead of just the series master. Without `expand=true`, a recurring weekly lecture returns one record, not every weekly instance. **Assistant view (which calendars you see by default).** The user can hide a calendar from your default view — `listFeeds` reports this as `agent_visible: false`. `listEvents` then excludes that calendar's events unless you pass `include_hidden=true` or request it by `feed_id`. So "what's on my calendar this week" answers from the user's chosen default set; if a user says "you're missing my Holidays calendar", retry with `include_hidden=true`. The calendar still syncs and stays visible in their app — this flag only scopes your default answers. To change it for the user, `PATCH /api/feeds/{id}` with `{agent_visible: true|false}`. **Idempotency:** supply `X-Idempotency-Key: ` on `POST /api/events`. If the same key is sent again within 5 minutes the original event is returned, not a duplicate. Use a per-request UUID, e.g. `agent-{session}-{uuid4}`. **Deletion is safe by default.** `DELETE /api/events/{id}` moves the event to Trash; it is recoverable for 30 days via `POST /api/events/{id}/restore`. Use `?permanent=true` only for events already in Trash that the user wants gone now. ### 3.3 Feeds (calendar subscriptions) | Method | Path | Purpose | | ------ | -------------------------- | -------------------------------------------------------------------- | | GET | `/api/feeds` | List the user's feeds. | | POST | `/api/feeds` | Add a new feed. Body: `{name, source_type, url_or_path, color, ...}`.| | GET | `/api/feeds/{id}` | Single feed. | | PATCH | `/api/feeds/{id}` | Update a feed. Body: `{name, color, enabled, agent_visible, account_group}`. | | DELETE | `/api/feeds/{id}` | Remove a feed. | | POST | `/api/feeds/{id}/sync` | Force a refresh of a remote feed. | | GET | `/api/feeds/{id}/events` | Events from one feed only. | ### 3.4 Personal store — todos, goals, projects, objectives These live as JSON blobs keyed by `kind`. Same shape the web app uses, so your edits are visible immediately on the user's dashboard. | Method | Path | Purpose | | ------ | --------------------- | ------------------------------------------------------------------- | | GET | `/api/store/{kind}` | Read the blob. `kind` is one of `todos`, `goals`, `projects`, `objectives`, `tweaks`. | | PUT | `/api/store/{kind}` | Replace the blob. Body: **`{"data": }`** — the blob must be wrapped in a `data` key. | | DELETE | `/api/store/{kind}` | Reset to empty. | **PUT body shape is critical.** The GET response has the shape `{"kind": "todos", "data": {...blob...}, "updated_at": "..."}`. When you PUT, you must send `{"data": {...blob...}}` — wrap the inner blob in `data`. Sending the raw blob at the top level will fail with a validation error. **Always GET before PUT.** PUT replaces the entire blob. Read the current state first, merge your changes into it, then PUT the full result back. Skipping the read will silently discard any fields you didn't include. #### Item-level operations (preferred for single-item edits) To add, edit, move, complete, or delete ONE task/goal/project/objective without the risk of a whole-blob PUT, use the item endpoints. These do a safe read-modify-write on the server and preserve every field you don't touch — the result appears in the user's dashboard immediately. | Method | Path | Purpose | | ------ | ------------------------------------------------ | --------------------------------------------------------- | | POST | `/api/store/{kind}/items` | Add one item. Body **is the item object** (no `data` wrap). | | PATCH | `/api/store/{kind}/items/{item_id}` | Merge fields into one item. Body is just the fields to change. | | DELETE | `/api/store/{kind}/items/{item_id}` | Remove one item. | | POST | `/api/store/todos/items/{item_id}/move` | Move a task between buckets. Body: `{"bucket": "week"}`. | | POST | `/api/store/todos/items/{item_id}/complete` | Mark a task done and move it to `completed`. | - `kind` is one of `todos`, `goals`, `projects`, `objectives`. - Identify an item by its `id` — except **objectives**, which are keyed by their ordinal label `n` (`"01"`, `"02"`, …). - For `todos`, choose the list with a `bucket` field **in the JSON body** = `today|week|later|completed|archived` (defaults to `today`). Goals/projects are appended; an objective is appended and assigned the next `n`. - An `id` is generated automatically if you don't supply one. The `id`/`n` of an existing item cannot be changed via PATCH. - A missing item returns `404`, so you can tell a no-op from a success. Note these are *additive* to GET/PUT — for bulk reorders or multi-item rewrites, the whole-blob PUT is still the right tool. **Schema cheat-sheet (current as of v5.17.x):** Goal: ```json { "id": "g-abc", "name": "Run 5km", "why": "build cardio base", "horizon": "quarter", "color": "var(--green)", "icon": "run", "pct": 67, "startPct": 0, "milestones": [ {"label": "Walk 5km", "state": "done"}, {"label": "Jog 3km", "state": "now"}, {"label": "Run 5km", "state": "todo"} ], "completed": false, // auto-set when pct reaches 100 "completed_at": null, // unix ms "archived": false, // manually stashed "archived_at": null } ``` Todo: ```json { "id": "t-abc", "title": "Draft proposal", "done": false, "completed_at": null, "priority": "med", // low | med | high "project": null, // optional project id "goal": null, // optional goal id "milestone": null // optional milestone index } ``` Todos live under buckets: `today`, `week`, `later`, `completed`, `archived`. ### 3.5 Reminders | Method | Path | Purpose | | ------ | --------------------------------- | -------------------------------- | | GET | `/api/reminders` | List reminders for the user. | | POST | `/api/reminders` | Create a reminder for an event. | | DELETE | `/api/reminders/{id}` | Remove a reminder. | ### 3.6 Share links (read-only public calendar views) | Method | Path | Purpose | | ------ | ----------------------------- | ------------------------------------------------------------ | | GET | `/api/share-links` | List the user's share links (each has a public `url`). | | POST | `/api/share-links` | Create one. Body: `{label, show_event_details, expires_at?, feed_ids?}`. | | PATCH | `/api/share-links/{id}` | Update label / visibility / expiry / included feeds. | | DELETE | `/api/share-links/{id}` | Revoke a link (its public URL stops working immediately). | `show_event_details:false` publishes busy/free blocks without titles. Omit `feed_ids` to share all calendars; omit `expires_at` for a link that never expires. ### 3.7 Settings | Method | Path | Purpose | | ------ | ----------------------- | -------------------------------------------------------- | | GET | `/api/settings` | List settings (merged with defaults), e.g. `timezone`. | | GET | `/api/settings/{key}` | Read one setting. | | PUT | `/api/settings/{key}` | Set one setting. Body: `{"value": "..."}`. | `timezone` is validated as an IANA name (e.g. `Australia/Brisbane`). ### 3.8 Hiding events (blacklist) Use this for noisy events from a remote feed you can't delete (society spam, league fixtures). Hiding suppresses an event from the unified view and shared calendars **without** touching the source feed. | Method | Path | Purpose | | ------ | --------------------------------- | --------------------------------------------------------- | | GET | `/api/blacklist` | List hidden events. | | POST | `/api/blacklist` | Hide an event. Body: `{event_uid, feed_id?, event_summary?, hide_from_share?}`. 409 if already hidden. | | DELETE | `/api/blacklist/{entry_id}` | Un-hide by entry id. | | DELETE | `/api/blacklist/by-uid/{uid}` | Un-hide every entry for an event UID. | | GET | `/api/blacklist/check/{uid}` | `{hidden: bool, entry_id}` for one UID. | Get the `event_uid` from a `listEvents` result (the `uid` field). > **Health & fitness** (sleep, bedtime, cognitive forecast, workouts/training > plans) lives under `/api/health/*` and is **opt-in per token**. A token can > reach it only if it was given the `health` permission at creation; otherwise > every `/api/health/*` call returns **403**. The endpoints and shapes are in a > separate schema: `https://ordent.us/ordentus-openapi-health.yaml`. Always check > `whoami → auth.can_access_health` first. --- ## 4. Worked examples ### Read today's events ```bash TODAY=$(date -u +%Y-%m-%dT00:00:00Z) TOMORROW=$(date -u -v+1d +%Y-%m-%dT00:00:00Z) # macOS; on Linux use -d '+1 day' curl -s -H "Authorization: Bearer $ORDENTUS_TOKEN" \ "https://ordent.us/api/events?start=$TODAY&end=$TOMORROW" | jq '.[].summary' ``` ### Add a 30-min focus block ```bash curl -s -X POST -H "Authorization: Bearer $ORDENTUS_TOKEN" \ -H "Content-Type: application/json" \ https://ordent.us/api/events \ -d '{ "summary": "Deep work", "dtstart": "2026-06-01T09:00:00+10:00", "dtend": "2026-06-01T09:30:00+10:00", "all_day": false }' ``` ### Add a todo to "today" (item-level — recommended) ```bash # Body IS the item object; no {"data": …} wrapper for item endpoints. # Put the target list in a "bucket" field in the body. curl -s -X POST -H "Authorization: Bearer $ORDENTUS_TOKEN" \ -H "Content-Type: application/json" \ "https://ordent.us/api/store/todos/items" \ -d '{"title": "Review PR #42", "priority": "med", "bucket": "today"}' # → 201 {"kind":"todos","item":{"id":"td…","title":"Review PR #42",…},"bucket":"today",…} ``` ### Edit, then complete that todo ```bash ID=td... # the id returned above curl -s -X PATCH -H "Authorization: Bearer $ORDENTUS_TOKEN" \ -H "Content-Type: application/json" \ "https://ordent.us/api/store/todos/items/$ID" \ -d '{"priority": "high"}' curl -s -X POST -H "Authorization: Bearer $ORDENTUS_TOKEN" \ "https://ordent.us/api/store/todos/items/$ID/complete" ``` ### Mark a goal complete (via milestones) Fetch the goal, set the last milestone to `state: "done"`, set `pct: 100`, and PATCH those two fields back with `/api/store/goals/items/{id}` (or PUT the whole `goals` blob). The dashboard's own auto-archive logic runs next time the user opens the page. --- ## 5. Errors | Code | Meaning | | ---- | -------------------------------------------------------------------- | | 401 | Missing or invalid token. | | 403 | Token doesn't carry the right scope for this method. | | 404 | The resource doesn't exist or belongs to another user. | | 409 | Conflict (e.g. duplicate username on register). | | 429 | Rate-limited. Back off and retry. | --- ## 6. Security and etiquette - **Treat tokens like passwords.** Never commit them. Never log them in plain text. Use environment variables or a secrets manager. - **Use the narrowest scope you need.** A summarising agent should ask for `read` only; only request `write` when you are sure the agent needs it. - **Revocation is immediate.** A user can revoke any token from Account Settings; admins can revoke from `/admin → API Tokens`. Once revoked, the token returns `401` on every request. - **Don't poll.** Most user data changes infrequently. If you need near-real-time, fetch on user action, not on a tight loop. - **Everything you do is logged and shown to the user.** Every API-token request (reads and writes) is recorded to a per-user activity log the user can review in Account Settings → API Access → Agent activity ("Created an event", "Retrieved events", …). Act as if the user is watching — because they are. --- Reference version: bundled with Ordentus v5.17.x. Endpoint shapes may evolve; this file is updated each release.