Developer Documentation
Integrate Loggd.life with your favorite tools. Use API tokens for read access and webhooks for real-time event notifications.
Overview
The Loggd.life API lets you integrate your personal growth data with external tools and automation platforms like Zapier, Make, n8n, or custom scripts.
API Tokens
Read-only access to your habits, tasks, goals, focus sessions, and profile data, scoped by ability.
Webhooks
Signed HTTP POST notifications when events occur in your account (habit completed, badge unlocked, level up, etc).
v1
Current version
JSON
Request / response
Bearer
Auth scheme
UTC
Timestamps
API access requires a Pro subscription. Manage tokens and webhooks from Settings → API. If you downgrade, your tokens remain but will return 403 until you re-subscribe.
Quickstart
From zero to your first API call in under 5 minutes.
- Go to Settings → API and click Create Token.
- Pick the scopes you need (e.g.
read:habits). Tokens are read-only. - Copy the token immediately — it’s only shown once.
- Call an endpoint:
# Replace YOUR_API_TOKEN with the token you just copied
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
https://loggd.life/api/v1/public/profileYou should receive a JSON response wrapped in a data field. If you see a 403 requires_pro, check your subscription. If you see a 403 missing_abilities, the token scope doesn’t match the endpoint.
Authentication
Every request must include a Bearer token in the Authorization header:
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
https://loggd.life/api/v1/public/habitsAvailable Scopes
read:habitsView your habits and completion historyread:tasksView your tasks and their statusread:goalsView your goals, milestones, and progressread:focusView your focus session historyread:profileView your profile information and gamification statsToken limits: up to 5 tokens per account. Tokens never expire automatically — revoke them from Settings when you’re done with a client.
All tokens are currently read-only. Write scopes (write:*) will be added in a future version.
Endpoints
All public endpoints live under /api/v1/public and require an API token with the matching scope.
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/public/habits | read:habits | List your habits |
| GET | /api/v1/public/habits/{id} | read:habits | Fetch a single habit |
| GET | /api/v1/public/habits/{id}/checks | read:habits | List habit check-ins (paginated) |
| GET | /api/v1/public/habits/{id}/skips | read:habits | List habit skips (rest days, sick days, etc.) |
| GET | /api/v1/public/tasks | read:tasks | List your tasks (top-level only; subtasks nest on their parent) |
| GET | /api/v1/public/tasks/{id} | read:tasks | Fetch a single task with subtasks and tags |
| GET | /api/v1/public/goals | read:goals | List your goals (with milestones and trend) |
| GET | /api/v1/public/goals/{id} | read:goals | Fetch a single goal |
| GET | /api/v1/public/goals/{id}/updates | read:goals | List metric update history for a goal |
| GET | /api/v1/public/focus-sessions | read:focus | List your focus sessions |
| GET | /api/v1/public/focus-sessions/{id} | read:focus | Fetch a single focus session |
| GET | /api/v1/public/profile | read:profile | Fetch profile info and gamification stats |
Endpoint Reference
/api/v1/public/habitsread:habitsList habits owned by the authenticated user. Archived habits are excluded by default.
Query parameters
status'active' (default), 'archived', or 'all'limitInteger 1-100 (default 50)afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/habitsExample response (200)
{
"data": [
{
"id": 42,
"name": "Morning Exercise",
"description": "30 minutes of cardio or strength",
"emoji": "💪",
"color": "#10B981",
"url": null,
"frequency": "daily",
"custom_days": null,
"tracking_mode": "check",
"weekly_target": null,
"daily_time_goal": null,
"data_source": null,
"allow_multiple_checks": false,
"display_order": 1,
"current_streak": 14,
"longest_streak": 45,
"status": "active",
"start_date": "2026-01-01",
"checked_today": true,
"last_checked_at": "2026-04-08T07:15:00+00:00",
"created_at": "2026-01-01T08:00:00+00:00",
"updated_at": "2026-03-15T10:00:00+00:00"
}
],
"has_more": true
}/api/v1/public/habits/{id}read:habitsFetch a single habit by id. Returns 404 if the habit does not exist or is owned by another user.
Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/habits/42Example response (200)
{
"data": {
"id": 42,
"name": "Morning Exercise",
"description": "30 minutes of cardio or strength",
"emoji": "💪",
"color": "#10B981",
"url": null,
"frequency": "daily",
"custom_days": null,
"tracking_mode": "check",
"weekly_target": null,
"daily_time_goal": null,
"data_source": null,
"allow_multiple_checks": false,
"display_order": 1,
"current_streak": 14,
"longest_streak": 45,
"status": "active",
"start_date": "2026-01-01",
"checked_today": true,
"last_checked_at": "2026-04-08T07:15:00+00:00",
"created_at": "2026-01-01T08:00:00+00:00",
"updated_at": "2026-03-15T10:00:00+00:00"
}
}/api/v1/public/habits/{id}/checksread:habitsList check-ins (completions) for a habit. Returns all checks by default (newest first). Optionally filter by date range. Only rows where the habit was actually checked are returned — skipped and unchecked days are excluded.
Query parameters
fromStart date (inclusive), YYYY-MM-DD. Optional — omit to include all history.toEnd date (inclusive), YYYY-MM-DD. Optional — omit for no upper bound.limitInteger 1-100 (default 50).afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://loggd.life/api/v1/public/habits/42/checks?limit=2"Example response (200)
{
"data": [
{
"id": 9801,
"date": "2026-04-08",
"source": "manual",
"note": "Felt great today",
"contribution_count": 1,
"checked_at": "2026-04-08T07:15:00+00:00"
},
{
"id": 9789,
"date": "2026-04-07",
"source": "manual",
"note": null,
"contribution_count": 1,
"checked_at": "2026-04-07T06:50:00+00:00"
}
],
"has_more": true
}/api/v1/public/habits/{id}/skipsread:habitsList days the user intentionally skipped a habit. Returns the reason (vacation, sick, rest, other) and optional note.
Query parameters
fromStart date (inclusive), YYYY-MM-DD. Optional — omit to include all history.toEnd date (inclusive), YYYY-MM-DD. Optional — omit for no upper bound.limitInteger 1-100 (default 50).afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://loggd.life/api/v1/public/habits/42/skips?limit=2"Example response (200)
{
"data": [
{
"id": 501,
"date": "2026-04-05",
"reason": "rest",
"note": "Recovery day"
},
{
"id": 488,
"date": "2026-03-20",
"reason": "sick",
"note": null
}
],
"has_more": false
}/api/v1/public/tasksread:tasksList top-level tasks owned by the authenticated user. Subtasks are NOT included in the list — they are nested under their parent on the single-task endpoint. By default, completed and cancelled tasks are excluded.
Query parameters
status'completed', 'cancelled', or 'all'. Defaults to active (neither completed nor cancelled).fromStart date (inclusive), YYYY-MM-DD. Filters by planned_day.toEnd date (inclusive), YYYY-MM-DD. Filters by planned_day.limitInteger 1-100 (default 50)afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://loggd.life/api/v1/public/tasks?from=2026-04-01&to=2026-04-30"Example response (200)
{
"data": [
{
"id": 123,
"title": "Review pull request",
"description": null,
"status": "scheduled",
"priority": "high",
"category": "work",
"source": null,
"planned_day": "2026-04-08",
"planned_week_start": "2026-04-06",
"due_time": "14:00",
"goal_id": null,
"goal_milestone_id": null,
"parent_task_id": null,
"completion_note": null,
"recurrence": null,
"subtasks": [],
"tags": [
{ "id": 5, "name": "Work", "color": "#3B82F6" }
],
"completed_at": null,
"cancelled_at": null,
"created_at": "2026-04-08T09:00:00+00:00",
"updated_at": "2026-04-08T09:30:00+00:00"
}
],
"has_more": true
}/api/v1/public/tasks/{id}read:tasksFetch a single task by id, including its nested subtasks, tags, and recurrence rule. Returns 404 if the task does not exist or is owned by another user.
Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/tasks/123Example response (200)
{
"data": {
"id": 123,
"title": "Weekly planning",
"description": "Review goals and plan the week",
"status": "scheduled",
"priority": "high",
"category": "personal",
"source": null,
"planned_day": "2026-04-08",
"planned_week_start": "2026-04-06",
"due_time": "09:00",
"goal_id": null,
"goal_milestone_id": null,
"parent_task_id": null,
"completion_note": null,
"recurrence": {
"is_template": true,
"parent_id": null,
"type": "weekly",
"interval": 1,
"days": ["monday"],
"based_on": "planned_day",
"ends_at": null,
"end_count": 0,
"completed_count": 12,
"next_occurrence_date": "2026-04-15"
},
"subtasks": [
{ "id": 901, "title": "Review last week", "status": "scheduled", "completed_at": null },
{ "id": 902, "title": "Plan this week", "status": "scheduled", "completed_at": null }
],
"tags": [
{ "id": 5, "name": "Work", "color": "#3B82F6" },
{ "id": 9, "name": "Personal", "color": "#F59E0B" }
],
"completed_at": null,
"cancelled_at": null,
"created_at": "2026-01-06T09:00:00+00:00",
"updated_at": "2026-04-07T09:00:00+00:00"
}
}/api/v1/public/goalsread:goalsList goals owned by the authenticated user, with their milestones, trend indicator, and on-track status. By default, only active goals are returned.
Query parameters
status'active' (default), 'completed', 'paused', 'abandoned', or 'all'time_horizon'monthly', 'quarterly', 'yearly', or '3_year'limitInteger 1-100 (default 50)afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://loggd.life/api/v1/public/goals?time_horizon=yearly"Example response (200)
{
"data": [
{
"id": 12,
"title": "Read 24 books",
"description": "Two books per month",
"notes": null,
"life_area": "growth",
"time_horizon": "yearly",
"tracking_type": "metric",
"status": "active",
"progress_percentage": 50,
"is_on_track": true,
"trend": "up",
"metric_unit": "books",
"metric_start_value": 0,
"metric_target_value": 24,
"metric_current_value": 12,
"metric_decrease": false,
"metric_value_type": null,
"last_update_note": null,
"started_at": "2026-01-01",
"target_date": "2026-12-31",
"completed_at": null,
"last_reviewed_at": "2026-04-01T10:00:00+00:00",
"updates_count": 12,
"milestones": [
{
"id": 45,
"title": "Finish first 6 books",
"completed": true,
"order": 1,
"target_date": "2026-03-31",
"completed_at": "2026-03-28T19:42:00+00:00"
}
],
"created_at": "2026-01-01T08:00:00+00:00",
"updated_at": "2026-04-01T10:00:00+00:00"
}
],
"has_more": false
}/api/v1/public/goals/{id}read:goalsFetch a single goal by id, with milestones inline. Use /goals/{id}/updates for the unbounded metric update history. The `trend` field is 'up', 'down', or 'neutral' based on the last 2 metric updates (respects `metric_decrease` for goals where lower is better, e.g. weight-loss). `is_on_track` is true when actual progress is within 10% of the expected pace toward the target date.
Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/goals/12Example response (200)
{
"data": {
"id": 12,
"title": "Read 24 books",
"description": "Two books per month",
"notes": null,
"life_area": "growth",
"time_horizon": "yearly",
"tracking_type": "metric",
"status": "active",
"progress_percentage": 50,
"is_on_track": true,
"trend": "up",
"metric_unit": "books",
"metric_start_value": 0,
"metric_target_value": 24,
"metric_current_value": 12,
"metric_decrease": false,
"metric_value_type": null,
"last_update_note": null,
"started_at": "2026-01-01",
"target_date": "2026-12-31",
"completed_at": null,
"last_reviewed_at": "2026-04-01T10:00:00+00:00",
"updates_count": 12,
"milestones": [
{
"id": 45,
"title": "Finish first 6 books",
"completed": true,
"order": 1,
"target_date": "2026-03-31",
"completed_at": "2026-03-28T19:42:00+00:00"
}
],
"created_at": "2026-01-01T08:00:00+00:00",
"updated_at": "2026-04-01T10:00:00+00:00"
}
}/api/v1/public/goals/{id}/updatesread:goalsList the metric update history for a goal — each row is a `GoalUpdate` record with the metric value and optional note at a point in time. Ordered newest first. This is the data you'd use to render a progress-over-time chart.
Query parameters
limitInteger 1-100 (default 50). Newest first.afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/goals/12/updatesExample response (200)
{
"data": [
{
"id": 890,
"metric_value": 12,
"note": "Finished 'Atomic Habits'",
"update_date": "2026-04-08T14:30:00+00:00",
"created_at": "2026-04-08T14:30:00+00:00"
},
{
"id": 871,
"metric_value": 11,
"note": null,
"update_date": "2026-03-25T09:00:00+00:00",
"created_at": "2026-03-25T09:00:00+00:00"
}
],
"has_more": true
}/api/v1/public/focus-sessionsread:focusList focus sessions owned by the authenticated user. By default, only completed sessions are returned.
Query parameters
status'completed' (default), 'active', 'cancelled', 'paused', or 'all'limitInteger 1-100 (default 50)afterCursor — the id of the last item from the previous page. Omit for the first page.Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://loggd.life/api/v1/public/focus-sessions?limit=5"Example response (200)
{
"data": [
{
"id": 321,
"title": "Deep work on API docs",
"status": "completed",
"started_at": "2026-04-08T14:00:00+00:00",
"ended_at": "2026-04-08T14:25:00+00:00",
"planned_duration_seconds": 1500,
"actual_duration_seconds": 1500,
"total_paused_seconds": 0,
"session_number": 5,
"points_earned": 4,
"task_id": 123,
"habit_id": null,
"notes": null,
"tags": [],
"created_at": "2026-04-08T14:00:00+00:00",
"updated_at": "2026-04-08T14:25:00+00:00"
}
],
"has_more": true
}/api/v1/public/focus-sessions/{id}read:focusFetch a single focus session by id.
Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/focus-sessions/321Example response (200)
{
"data": {
"id": 321,
"title": "Deep work on API docs",
"status": "completed",
"started_at": "2026-04-08T14:00:00+00:00",
"ended_at": "2026-04-08T14:25:00+00:00",
"planned_duration_seconds": 1500,
"actual_duration_seconds": 1500,
"total_paused_seconds": 0,
"session_number": 5,
"points_earned": 4,
"task_id": 123,
"habit_id": null,
"notes": null,
"tags": [
{ "id": 3, "name": "Deep Work", "color": "#8B5CF6" }
],
"created_at": "2026-04-08T14:00:00+00:00",
"updated_at": "2026-04-08T14:25:00+00:00"
}
}/api/v1/public/profileread:profileReturn profile info and gamification stats for the authenticated user. The `avatar_id` field is a string key — resolve it to an image via your own registry, or fetch the static asset from Loggd (e.g. `/images/avatars/{avatar_id}.svg`). `subscription_expires_at` is null for lifetime accounts.
Example request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://loggd.life/api/v1/public/profileExample response (200)
{
"data": {
"id": 7,
"username": "jane",
"email": "jane@example.com",
"bio": "Building better habits one day at a time",
"avatar_id": "fox",
"avatar_animated": true,
"timezone": "Europe/Bucharest",
"week_start_day": "monday",
"level": 7,
"total_points": 2500,
"monthly_points": 350,
"current_streak": 14,
"longest_streak": 45,
"subscription_tier": "pro",
"subscription_expires_at": "2027-01-01T00:00:00+00:00",
"created_at": "2025-11-01T08:00:00+00:00"
}
}Response Format
All successful responses follow a consistent envelope. Single-resource endpoints return data as an object; collection endpoints return data as an array.
// Collection endpoint
{
"data": [ { /* resource */ }, { /* resource */ } ]
}
// Single-resource endpoint
{
"data": { /* resource */ }
}Timestamps & dates
- Datetimes use ISO 8601 in UTC:
2026-04-08T14:30:00+00:00 - Dates (no time component) use
YYYY-MM-DD - Missing datetimes are returned as
null, never as empty strings
Field stability
Additive changes (new fields) can appear in any v1 release and are not breaking. Treat unknown fields as optional in your deserializer. Field removals or type changes will bump the version to v2.
Errors
Errors return JSON with a message field and a machine-readable flag where applicable.
| Status | Meaning | How to fix |
|---|---|---|
401 | Missing or invalid token | Check the Authorization header. Make sure you're using a real token (not a session cookie). |
403 | Pro subscription required or wrong scope | Check requires_pro or missing_abilities in the response body. Re-subscribe or create a token with the correct scopes. |
404 | Resource not found | The id doesn't exist or belongs to another user. We never leak existence across users. |
422 | Validation error | The errors field lists which inputs failed validation and why. |
429 | Rate limit exceeded | Slow down and check Retry-After. See the Rate Limits section. |
500 | Server error | Something broke on our end. Retry with backoff; if it persists, contact support. |
Example error payloads
401 Unauthorized
{
"message": "Authentication required. Include a valid API token in the Authorization header."
}403 Pro required
{
"message": "API access requires an active Pro subscription.",
"requires_pro": true
}403 Missing scope
{
"message": "Your API token is missing the required scope for this endpoint.",
"missing_abilities": ["read:habits"]
}404 Not found
{
"message": "Habit not found."
}Webhooks
Webhooks send signed POST requests to your URL when events happen in your account. Set them up from Settings → API.
Payload Format
{
"event": "habit.completed",
"version": 1,
"timestamp": "2026-04-08T14:30:00+00:00",
"data": {
"habit_id": 456,
"habit_name": "Morning Exercise",
"check_date": "2026-04-08",
"streak": 7
}
}All webhook payloads share this envelope: the event name, a version integer (currently 1), an ISO 8601 timestamp, and a data object whose shape depends on the event type. The version will increment if the envelope structure or a specific event's data shape changes in a breaking way.
Request Headers
| Header | Description |
|---|---|
X-Loggd-Signature | HMAC-SHA256 signature of the raw body (sha256=<hex>) |
X-Loggd-Event | Event type (e.g. habit.completed) |
X-Loggd-Delivery | Unique delivery id — use as a dedup key |
Content-Type | application/json |
User-Agent | Loggd-Webhook/1.0 |
Delivery & Retries
- Webhook URLs must use HTTPS in production
- Requests timeout after 30 seconds
- We consider any
2xxresponse a success - Non-2xx responses trigger retries up to 5 times with exponential backoff: immediate, 1 min, 5 min, 15 min, 1 hour
- After 10 consecutive failures the endpoint is automatically deactivated
- You can create up to 5 webhook endpoints per account
- Your handler should be idempotent — retries mean the same delivery can arrive more than once (use
X-Loggd-Deliveryas a dedup key) - Delivery log history is retained; recent attempts are visible in Settings → API → each webhook row
Local Development
To test webhooks locally, expose your dev server with a tool like ngrok or Tailscale Funnel, then register the public URL as your webhook endpoint. Use the Send test event button in Settings to trigger a delivery without performing a real action.
Webhook Events
habit.createdFired when a new habit is created.
Example data
{
"habit_id": 456,
"habit_name": "Morning Exercise",
"frequency": "daily",
"tracking_mode": "check",
"created_at": "2026-01-01T08:00:00+00:00"
}habit.archivedFired when a habit is archived.
Example data
{
"habit_id": 456,
"habit_name": "Morning Exercise",
"longest_streak": 45
}habit.completedFired when a habit check is recorded for the day.
Example data
{
"habit_id": 456,
"habit_name": "Morning Exercise",
"check_date": "2026-04-08",
"streak": 7
}task.createdFired when a new top-level task is created. Subtasks do not fire this event — the parent carries the meaningful signal.
Example data
{
"task_id": 123,
"task_title": "Review pull request",
"priority": "high",
"planned_day": "2026-04-08",
"goal_id": null,
"is_recurring": false
}task.completedFired when a task is marked as completed.
Example data
{
"task_id": 123,
"task_title": "Review pull request",
"is_goal_task": false,
"goal_id": null,
"completed_at": "2026-04-08T14:30:00+00:00"
}task.cancelledFired when a top-level task is cancelled.
Example data
{
"task_id": 123,
"task_title": "Review pull request",
"cancelled_at": "2026-04-08T14:30:00+00:00"
}goal.createdFired when a new goal is created.
Example data
{
"goal_id": 12,
"goal_title": "Read 24 books",
"life_area": "growth",
"time_horizon": "yearly",
"tracking_type": "metric",
"target_date": "2026-12-31"
}goal.progress_changedFired when a goal crosses a progress milestone (25%, 50%, 75%, 90%). Does NOT fire on every metric tick — only on threshold crossings.
Example data
{
"goal_id": 12,
"goal_title": "Read 24 books",
"previous_progress": 45,
"new_progress": 52,
"milestone_crossed": 50
}goal.completedFired when a goal is marked as completed.
Example data
{
"goal_id": 12,
"goal_title": "Read 24 books",
"time_horizon": "yearly",
"progress_percentage": 100,
"completed_at": "2026-04-08T14:30:00+00:00"
}goal.milestone_reachedFired when a goal milestone is completed.
Example data
{
"goal_id": 12,
"goal_title": "Read 24 books",
"milestone_id": 45,
"milestone_title": "Finish first 6 books",
"progress_percentage": 50
}focus_session.startedFired when a focus session begins. Does NOT fire for manual retro entries (sessions created directly with status=completed).
Example data
{
"session_id": 321,
"title": "Deep work on API docs",
"planned_duration": 1500,
"task_id": 123,
"habit_id": null,
"started_at": "2026-04-08T14:00:00+00:00"
}focus_session.completedFired when a focus session ends.
Example data
{
"session_id": 321,
"title": "Deep work on API docs",
"duration_seconds": 1500,
"planned_duration": 1500
}badge.unlockedFired when a new badge is earned.
Example data
{
"badge_key": "streak_warrior_30",
"badge_name": "Streak Warrior",
"badge_rarity": "rare",
"points_earned": 100
}level.upFired when the user reaches a new level.
Example data
{
"new_level": 5,
"previous_level": 4,
"tier_name": "Adventurer",
"total_points": 2500
}checkin.completedFired when a daily check-in is submitted.
Example data
{
"checkin_id": 789,
"date": "2026-04-08"
}Signature Verification
Every webhook delivery is signed with your endpoint’s secret using HMAC-SHA256. Always verify the signature before trusting the payload.
The signature is sent in the X-Loggd-Signature header as sha256=<hex_digest>. Compute the HMAC over the raw request body (not a re-serialized copy) and use a constant-time comparison.
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Constant-time comparison — use Buffer.from to avoid timing attacks.
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: use express.raw() so req.body is the raw buffer.
app.post(
'/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-loggd-signature'];
if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
console.log(payload.event, payload.data);
res.sendStatus(200);
}
);Rotating the secret: if you ever suspect your secret is compromised, rotate it from Settings → API. The old secret is invalidated immediately, so deploy the new secret before rotating (or be ready for a short window of signature failures).
Rate Limits
API requests are rate-limited per user. Hitting the limit returns 429 Too Many Requests.
| Bucket | Limit | Window |
|---|---|---|
| Public API (/api/v1/public/*) | 300 | per minute |
| Token management | 60 | per minute |
| Webhook management | 60 | per minute |
Rate limit headers are included on every response:
X-RateLimit-Limit— the total requests allowed in the current windowX-RateLimit-Remaining— requests remaining before the next resetRetry-After— present on429responses, in seconds
Changelog
v1.12026-04-08- Habits: added `url`, `custom_days`, `data_source`, `allow_multiple_checks`, and `display_order` fields to list and show endpoints
- Habits: added `checked_today` and `last_checked_at` fields on the list and show endpoints
- Habits: new `GET /habits/{id}/checks` endpoint with `from`/`to`/`limit` query params
- Habits: new `GET /habits/{id}/skips` endpoint — lists intentional skip days with reason and note
- Tasks: added `source`, `goal_milestone_id`, and `completion_note` fields to list and show endpoints
- Tasks: added nested `subtasks`, `tags`, and `recurrence` objects on the show endpoint
- Tasks: list endpoint now returns only top-level tasks (subtasks are accessed via the parent). This is a breaking change from v1.0 — subtasks were previously returned as top-level rows.
- Tasks: added `from`/`to` date range query params (filter by planned_day)
- Goals: added `notes`, `metric_decrease`, `metric_value_type`, and `last_update_note` fields; milestones now include `order`
- Goals: added `is_on_track`, `trend`, `updates_count`, and `last_reviewed_at` fields
- Goals: new `GET /goals/{id}/updates` endpoint for metric update history
- Goals: show endpoint returns milestones inline (bounded); updates are accessed via the separate /updates endpoint (unbounded)
- Focus: added `total_paused_seconds`, `session_number`, `points_earned`, and `tags` fields
- Focus: fixed `planned_duration_seconds` — now correctly returns seconds (was previously returning the raw minutes value)
- Profile: added `week_start_day` and `monthly_points` fields
- Profile: added `bio`, `avatar_id`, `avatar_animated`, and `subscription_expires_at` fields
- Webhooks: 7 new lifecycle events — habit.created, habit.archived, task.created, task.cancelled, goal.created, goal.progress_changed, focus_session.started
v1.02026-04-08- Initial public release of the /api/v1/public/* read-only API
- Five read scopes: read:habits, read:tasks, read:goals, read:focus, read:profile
- Webhook events: habit.completed, task.completed, goal.completed, goal.milestone_reached, badge.unlocked, level.up, checkin.completed, focus_session.completed
- HMAC-SHA256 signed webhook deliveries with exponential-backoff retries
Ready to integrate?
Create Your First Token