loggd.life loggd.life

Developer Documentation

Integrate Loggd.life with your favorite tools. Use API tokens for read access and webhooks for real-time event notifications.

API Status: Operational · v1

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.

  1. Go to Settings → API and click Create Token.
  2. Pick the scopes you need (e.g. read:habits). Tokens are read-only.
  3. Copy the token immediately — it’s only shown once.
  4. 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/profile

You 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/habits

Available Scopes

read:habitsView your habits and completion history
read:tasksView your tasks and their status
read:goalsView your goals, milestones, and progress
read:focusView your focus session history
read:profileView your profile information and gamification stats

Token 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.

MethodPathScopeDescription
GET/api/v1/public/habitsread:habitsList your habits
GET/api/v1/public/habits/{id}read:habitsFetch a single habit
GET/api/v1/public/habits/{id}/checksread:habitsList habit check-ins (paginated)
GET/api/v1/public/habits/{id}/skipsread:habitsList habit skips (rest days, sick days, etc.)
GET/api/v1/public/tasksread:tasksList your tasks (top-level only; subtasks nest on their parent)
GET/api/v1/public/tasks/{id}read:tasksFetch a single task with subtasks and tags
GET/api/v1/public/goalsread:goalsList your goals (with milestones and trend)
GET/api/v1/public/goals/{id}read:goalsFetch a single goal
GET/api/v1/public/goals/{id}/updatesread:goalsList metric update history for a goal
GET/api/v1/public/focus-sessionsread:focusList your focus sessions
GET/api/v1/public/focus-sessions/{id}read:focusFetch a single focus session
GET/api/v1/public/profileread:profileFetch profile info and gamification stats

Endpoint Reference

GET/api/v1/public/habitsread:habits

List 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/habits

Example 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
}
GET/api/v1/public/habits/{id}read:habits

Fetch 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/42

Example 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"
  }
}
GET/api/v1/public/habits/{id}/checksread:habits

List 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
}
GET/api/v1/public/habits/{id}/skipsread:habits

List 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
}
GET/api/v1/public/tasksread:tasks

List 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
}
GET/api/v1/public/tasks/{id}read:tasks

Fetch 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/123

Example 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"
  }
}
GET/api/v1/public/goalsread:goals

List 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
}
GET/api/v1/public/goals/{id}read:goals

Fetch 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/12

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"
  }
}
GET/api/v1/public/goals/{id}/updatesread:goals

List 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/updates

Example 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
}
GET/api/v1/public/focus-sessionsread:focus

List 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
}
GET/api/v1/public/focus-sessions/{id}read:focus

Fetch a single focus session by id.

Example request

curl -H "Authorization: Bearer YOUR_API_TOKEN" \
     https://loggd.life/api/v1/public/focus-sessions/321

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": [
      { "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"
  }
}
GET/api/v1/public/profileread:profile

Return 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/profile

Example 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.

StatusMeaningHow to fix
401Missing or invalid tokenCheck the Authorization header. Make sure you're using a real token (not a session cookie).
403Pro subscription required or wrong scopeCheck requires_pro or missing_abilities in the response body. Re-subscribe or create a token with the correct scopes.
404Resource not foundThe id doesn't exist or belongs to another user. We never leak existence across users.
422Validation errorThe errors field lists which inputs failed validation and why.
429Rate limit exceededSlow down and check Retry-After. See the Rate Limits section.
500Server errorSomething 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

HeaderDescription
X-Loggd-SignatureHMAC-SHA256 signature of the raw body (sha256=<hex>)
X-Loggd-EventEvent type (e.g. habit.completed)
X-Loggd-DeliveryUnique delivery id — use as a dedup key
Content-Typeapplication/json
User-AgentLoggd-Webhook/1.0

Delivery & Retries

  • Webhook URLs must use HTTPS in production
  • Requests timeout after 30 seconds
  • We consider any 2xx response 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-Delivery as 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.created

Fired 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.archived

Fired when a habit is archived.

Example data

{
  "habit_id": 456,
  "habit_name": "Morning Exercise",
  "longest_streak": 45
}
habit.completed

Fired 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.created

Fired 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.completed

Fired 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.cancelled

Fired 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.created

Fired 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_changed

Fired 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.completed

Fired 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_reached

Fired 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.started

Fired 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.completed

Fired when a focus session ends.

Example data

{
  "session_id": 321,
  "title": "Deep work on API docs",
  "duration_seconds": 1500,
  "planned_duration": 1500
}
badge.unlocked

Fired when a new badge is earned.

Example data

{
  "badge_key": "streak_warrior_30",
  "badge_name": "Streak Warrior",
  "badge_rarity": "rare",
  "points_earned": 100
}
level.up

Fired when the user reaches a new level.

Example data

{
  "new_level": 5,
  "previous_level": 4,
  "tier_name": "Adventurer",
  "total_points": 2500
}
checkin.completed

Fired 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.

BucketLimitWindow
Public API (/api/v1/public/*)300per minute
Token management60per minute
Webhook management60per minute

Rate limit headers are included on every response:

  • X-RateLimit-Limit — the total requests allowed in the current window
  • X-RateLimit-Remaining — requests remaining before the next reset
  • Retry-After — present on 429 responses, 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