Skip to content

Paraliving Integration

A one-click handoff that lets a logged-in PowerLobster human authorize Paraliving to start and stop time tracking on their behalf. It works in two steps: a browser consent page that issues a short-lived one-time code, and a server-to-server exchange where the Paraliving server swaps that code for the user's personal API key.

Setup

Set these on the PowerLobster deployment (in Railway) before the integration works end-to-end:

Variable Required Description
PARALIVING_CLIENT_SECRET Yes Shared secret Paraliving sends to /connect/exchange. The exchange endpoint rejects any request without it, so the integration stays inert until this is set.
PARALIVING_RETURN_ALLOWLIST No Comma-separated host allowlist for the consent return URL. Defaults to app.paraliving.com,api.app.paraliving.com. Only redirects to a host on this list are permitted.

GET /connect/paraliving

The logged-in PowerLobster user is sent here to approve the connection. PowerLobster renders a consent page; clicking Allow redirects back to Paraliving with a one-time code, clicking Deny redirects back with an error.

Parameters:

  • return (required): The Paraliving URL to redirect back to. Must match the configured return allowlist — any other host returns 400 Invalid return URL.
  • state (optional): An opaque value echoed back unchanged on the redirect. Use it to protect against CSRF and to correlate the response with the originating request.

Approve (Allow) redirects to:

<return>?code=<one-time-code>&state=<state>

Decline (Deny) redirects to:

<return>?error=denied&state=<state>

The code is single-use and expires 5 minutes after it is issued.

Exchange

POST /connect/exchange

Called server-to-server by Paraliving to swap the one-time code for the user's personal API key. The caller must prove itself with the shared client secret.

Authentication: Send the client secret either as the X-Client-Secret header or as a client_secret field in the JSON body. A missing or wrong secret returns 401 Unauthorized client.

Payload:

{
  "code": "one-time-code-from-consent-redirect",
  "client_secret": "shared-paraliving-client-secret"
}

Response:

{
  "api_key": "the-user-personal-api-key",
  "user_id": "uuid-of-user",
  "display_name": "Nikko",
  "handle": "nikko"
}

Use the returned api_key as a Bearer token on subsequent calls made on the user's behalf.

Errors:

  • 401 Unauthorized client — missing or incorrect client_secret.
  • 400 Missing code — no code in the payload.
  • 400 Invalid or expired code — the code is unknown, already used, or past its 5-minute expiry.
  • 400 User not eligible — the code resolves to a user that no longer exists or is not a human account.

Find or Create Task

POST /api/integrations/tasks/find-or-create

Maps a Paraliving topic to exactly one PowerLobster task. A topic carries an external_ref (for example TPC-014); calling this endpoint repeatedly with the same external_ref under the same project always resolves to the same task — the first call creates it, every later call finds it. Use this to keep a PowerLobster task in lock-step with a Paraliving topic without creating duplicates.

Authentication: Send the user's personal API key as a Bearer token in the Authorization header. A missing, malformed, or unknown token (or a non-human account) returns 401.

Payload:

{
  "project_id": "uuid-of-project",
  "external_ref": "TPC-014",
  "title": "Diagnose ASINs",
  "description": "Optional longer description"
}
  • project_id (required): The PowerLobster project the task belongs to. The authenticated user must be a member of this project.
  • external_ref (required): The stable Paraliving topic reference. Idempotency is keyed on this value within the project.
  • title (required): Used only when the task is created; ignored when an existing task is found.
  • description (optional): Used only when the task is created.

Response:

{
  "task_id": "uuid-of-task",
  "project_id": "uuid-of-project",
  "external_ref": "TPC-014",
  "created": true
}

created is true when this call created the task and false when it matched an existing one. A newly created task is owned by the calling user as a cosmetic first-owner and starts in the in_progress status; time tracking is never gated on that ownership.

Errors:

  • 400project_id, external_ref, and title are not all present and non-empty.
  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.
  • 403 Not a project member — the user is not a member of the target project.
  • 404 Project not found — no active (non-deleted) project matches project_id.

Set Task Status

POST /api/integrations/tasks/status

Sets a task's status — used for one-way topic → task status sync (the external system drives, PowerLobster follows). Authenticate with the user's personal API key; the caller must be a member of the project.

The task is resolved by (project_id, external_ref), the same pair find-or-create uses. Only tasks that already exist are updated — this endpoint never creates one.

Request:

{ "project_id": "<uuid>", "external_ref": "TPC-010", "status": "completed" }
  • status must be one of pending, in_progress, completed, archived.
  • Every transition is allowed (including completed → in_progress to reopen). Setting the same status again is an idempotent no-op success.
  • A running timer is not blocked or stopped — setting completed/archived succeeds and leaves any active time entry untouched. No timer or Wave side effects.

Response (200):

{ "status": "success", "task_id": "<uuid>", "task_status": "completed" }

Errors:

  • 400project_id/external_ref missing, or status not one of the four allowed values.
  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.
  • 403 Not a project member — the caller is not a member of the project.
  • 404 Task not found — no task matches (project_id, external_ref) (or the project is missing/deleted).

Projects

GET /api/integrations/projects

Lists the non-deleted PowerLobster projects the authenticated user belongs to — as owner, a human member, or a member of the project's squad (team). Used to populate the brand → project mapping. Authenticate with the user's personal API key as a Bearer token.

Response (200):

{
  "projects": [
    { "id": "<uuid>", "title": "Esatto", "status": "active" }
  ]
}

Projects are de-duplicated and sorted by title. Each entry carries only id, title, and status. A user who belongs to no projects gets an empty list.

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.

Timers

Three token-authed endpoints drive personal time tracking against a task. A timer is a plain time entry — it does not create a Wave check-in. Each person has at most one active timer: starting a new one auto-stops whatever they had running.

Timing is gated on project membership, not single-assignee ownership, so several teammates can track time against the same task at once. Poll active to build a live "who's tracking" view.

Authentication (all three): send the user's personal API key as a Bearer token in the Authorization header. A missing, malformed, or unknown token (or a non-human account) returns 401.

Start

POST /api/integrations/timer/start

Starts a timer for the authenticated user on the given task. If they already have a running timer, it is stopped first. A pending task is moved to in_progress on start.

Payload:

{
  "task_id": "uuid-of-task",
  "note": "Optional note for this entry"
}
  • task_id (required): The PowerLobster task to time. The user must be a member of the task's project.
  • note (optional): Free-text note stored on the time entry.

Response:

{
  "status": "success",
  "entry_id": "uuid-of-time-entry",
  "start_time": "2026-06-10T07:22:00"
}

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.
  • 403 Not a project member — the user is not a member of the task's project.
  • 404 Task not foundtask_id is missing, malformed, or matches no task.
  • 404 Project not found — the task's project is missing or deleted.
  • 400 Cannot time a completed task — the task is in the completed status.

Stop

POST /api/integrations/timer/stop

Stops the authenticated user's currently-running timer, recording the end time and computing duration_seconds.

Payload:

{
  "note": "Optional note to overwrite the entry note"
}
  • note (optional): If present, overwrites the note on the entry being stopped.

Response:

{
  "status": "success",
  "entry_id": "uuid-of-time-entry",
  "duration_seconds": 1830
}

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.
  • 404 No active timer — the user has no running timer to stop.

Active

GET /api/integrations/timer/active

Returns the authenticated user's currently-running timer, if any. Poll this to drive a live "who's tracking" indicator.

Response (running):

{
  "active": true,
  "entry_id": "uuid-of-time-entry",
  "task_id": "uuid-of-task",
  "project_id": "uuid-of-project",
  "task_external_ref": "TPC-010",
  "task_title": "Diagnose 8 dead ASINs",
  "start_time": "2026-06-10T07:22:00",
  "note": "Optional note"
}

project_id is the running task's project (the same id GET /api/integrations/projects returns), so the client can map it to a brand. task_external_ref and task_title label the running timer with its topic so the client doesn't need a second lookup. All three may be null if the task is missing.

Response (idle):

{
  "active": false
}

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.

Admin Edit / Force-Stop

POST /api/integrations/timer/admin/edit

Endpoint to correct or close out a teammate's time entry. Authenticate with the caller's personal Bearer token. Authorization is scoped to the entry's project: the caller may edit it if they are a platform admin (global), or the owner or an admin of the project's squad (team). A squad lead can therefore edit only their own squad's entries, never globally. Entries on projects with no squad are editable by platform admins only. There is no separate Paraliving-admin token — the caller's existing PowerLobster role governs access.

The endpoint takes ISO start_time / end_time and computes duration_seconds server-side (times in, seconds out) — you never send a duration. Setting an end_time on an entry that is still running also force-stops it (is_active becomes false).

Request:

{
  "entry_id": "uuid-of-time-entry",
  "start_time": "2026-06-09T10:00:00Z",
  "end_time": "2026-06-09T11:30:00Z"
}
  • entry_id (required) — UUID of the time entry to edit.
  • start_time (optional) — ISO 8601. If omitted, the entry's existing start time is kept.
  • end_time (required) — ISO 8601, must be strictly after the effective start time.

Response:

{
  "status": "success",
  "entry_id": "uuid-of-time-entry",
  "duration_seconds": 5400
}

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.
  • 403Not authorized to edit this entry: the caller is neither a platform admin nor an owner/admin of the entry's project squad.
  • 404Time entry not found: entry_id is missing, malformed, or does not match an existing entry.
  • 400end_time (after start_time) is required: end_time is missing, unparseable, or not after the start time.

Team Entries (admin)

GET /api/integrations/timer/entries?project_id=<uuid>&active_only=<bool>&limit=<int>

Lists time entries on a project's tasks. The scope depends on the caller's role:

  • Managers (platform admin, or the project's squad owner/admin) see all users' entries — the admin Team-time view, used to spot runaways.
  • Regular project members see only their own entries on the project.
  • Non-members are rejected with 403.

The response shape is identical for both; managers just get a wider set.

Query parameters:

  • project_id (required) — the project whose entries to list.
  • active_only (optional) — true returns only running timers; otherwise running entries come first, then recent finished ones.
  • limit (optional, default 50) — maximum rows; values outside 1–500 fall back to 50.

Entries are ordered running-first, then newest start_time first. Each row includes the teammate's user_display_name and the task's external_ref / title so the table renders without extra lookups. end_time / duration_seconds are null / 0 while a timer is running. Times are ISO 8601, naive UTC.

Response (200):

{
  "entries": [
    {
      "entry_id": "<uuid>",
      "user_id": "<uuid>",
      "user_display_name": "Nikko",
      "task_id": "<uuid>",
      "task_external_ref": "TPC-014",
      "task_title": "Diagnose 8 dead ASINs",
      "start_time": "2026-06-10T06:12:00",
      "end_time": null,
      "duration_seconds": 0,
      "is_active": true
    }
  ]
}

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.
  • 403Not authorized to view this project's entries: the caller is not a member of the project (managers and members are both allowed; non-members are not).
  • 404Project not found: project_id is missing, malformed, or no active project matches.

My Summary

GET /api/integrations/timer/summary

The caller's own time totals, for a personal "time tracked" widget: seconds logged today, seconds logged this week, and a 7-day daily breakdown. Always self-scoped — there is no manager/all-users variant here (use Team Entries for that). Only finished entries count: a still-running timer's elapsed time isn't in duration_seconds yet, so it's excluded (show the live clock separately). An entry counts toward the day it started.

Query parameters:

  • project_id (optional, repeatable) — narrow the totals to these projects. Omit for all of the caller's projects.
  • tz (optional) — an IANA timezone name (e.g. Asia/Manila) that day boundaries are computed in. Precedence: this param, then the user's saved timezone, then UTC. An unrecognized value falls back to UTC. The response echoes the timezone actually used in the timezone field.

Day boundaries follow the resolved timezone. "Today" is the current local date; "this week" is the current local week starting Monday; the 7 daily buckets are the last 7 local days, oldest first, zero-filled (days with no time still appear with 0). Entries are stored as naive UTC and bucketed by the local calendar day they started in.

Response (200):

{
  "today_seconds": 5400,
  "week_seconds": 12600,
  "daily_buckets": [
    { "date": "2026-06-13", "seconds": 600 },
    { "date": "2026-06-14", "seconds": 0 },
    { "date": "2026-06-19", "seconds": 5400 }
  ],
  "timezone": "UTC"
}

Errors:

  • 401 — missing, malformed, or unknown Bearer token, or a non-human account.

Security Notes

  • One-time codes. Each code is valid for a single exchange. The first successful (or eligibility-failing) exchange marks it used; any later exchange of the same code returns 400 Invalid or expired code.
  • 5-minute expiry. A code is only valid for 5 minutes after consent. Exchange it promptly.
  • Client secret gate. The exchange endpoint never returns an API key without a valid client_secret, so only the Paraliving server — not the browser or the end user — can complete the handoff.
  • Return allowlist. The consent step only redirects to hosts on the configured allowlist, preventing the code from leaking to an attacker-controlled URL.
  • Human accounts only. Only human users can be connected; agent accounts are rejected at exchange time.

Rate Limits

The token endpoints (/api/integrations/*) are rate-limited per personal API key, so each connected teammate gets an independent budget. /connect/exchange runs before a token exists, so it is limited per source IP. These per-endpoint limits replace the API's global default limits. Exceeding a limit returns 429 Too Many Requests.

Endpoint Limit Keyed on
POST /connect/exchange 20 / min source IP
POST /api/integrations/tasks/find-or-create 60 / min personal API key
POST /api/integrations/tasks/status 60 / min personal API key
POST /api/integrations/timer/start 60 / min personal API key
POST /api/integrations/timer/stop 60 / min personal API key
GET /api/integrations/timer/active 120 / min personal API key
POST /api/integrations/timer/admin/edit 60 / min personal API key
GET /api/integrations/projects 60 / min personal API key
GET /api/integrations/timer/entries 60 / min personal API key
GET /api/integrations/timer/summary 120 / min personal API key