Plant Journal

Access keys and the API

Version 1 · last updated 27 October 2026

What this is

An access key lets another app use your household's plants without signing in as you. Make one in the app under Settings > Access keys. There are three kinds:

A key is shown once when you make it. Keep it secret like a password, make one per app, and revoke it in Settings when you no longer need it. Only a household member can make or revoke keys, and a key only ever reaches its own household's data. Settings also shows when each key was last used and a record of what keys did.

The calendar link

Make a key of the kind Calendar link. The app shows the full address, which looks like this:

https://guglanruymczrxailwny.supabase.co/functions/v1/api/v1/calendar.ics?key=pj_…

In your calendar app choose “subscribe to a calendar” (or “add by URL”) and paste it. Each plant's next watering, feeding, misting and repotting appears on its due day (overdue ones sit on today), and so does each open reminder, with its note. When you mark something done in Plant Journal the event moves on the next refresh; calendar apps usually refresh every few hours. A calendar link may be fetched 30 times an hour.

Calling it from a program

Send the key in the Authorization header. The base address is:

https://guglanruymczrxailwny.supabase.co/functions/v1/api/v1
curl -H "Authorization: Bearer pj_…" \
  "https://guglanruymczrxailwny.supabase.co/functions/v1/api/v1/due?days=3"

(Only calendar keys may be sent in the web address, because calendar apps cannot send headers.) Answers are JSON. Dates are YYYY-MM-DD in Helsinki time, the same day the app and the daily email use. Version 1 will not change in ways that break programs: fields may be added, never renamed or removed.

Reading

GET /plants — your plants (?include_archived=1 adds archived ones). Each has id, name, genus, species, light, height_cm, the care intervals and last-done dates, cutting_since, photo_url, notes, and next_due (the next due date per kind of task).

GET /due — what is due today or overdue, with ?days=N (0 to 14) to look ahead. Gives tasks (plant_id, plant, task as water, fert, mist or repot, due, overdue_days) and open reminders. It follows the same rules as the app: a cutting is watered every 2 days and not fed or misted, snoozed tasks return on their day, and task kinds the owner switched off, never-logged tasks and archived plants are left out.

GET /reminders — all open reminders with their notes.

GET /history — completed tasks, newest first (?days=30, up to 90; at most 500).

GET /calendar.ics — the calendar file described above (any read or calendar key).

Marking things done

Needs a key of the kind Read and mark done.

POST /tasks/done with a JSON body:

{"plant_id": "p_123", "task": "water"}

It works like the app's “Mark done”: it records today as the last time, clears a snooze, and adds a line to the care history (with how late it was). Repotting also writes the pot history and, for a cutting, moves the cutting stage and its check-in reminder. The same call twice on one day changes nothing the second time (the answer says "already_done_today": true), so a program that retries cannot log it twice. Archived plants, a cutting’s feeding and misting, tasks the plant has no schedule for and task kinds the owner switched off are refused with 409.

POST /reminders/done with {"id": "r_123"} marks a reminder done.

Webhooks

Instead of asking again and again, an app can be told. On a key of the kind Read only or Read and mark done (not a calendar link), choose Add a webhook in Settings > Access keys: give an https address and the events you want. A key has at most one webhook, and a household at most 3. The page shows a signing secret (whsec_…) once.

Each message is a POST with a JSON body:

{"id": "…", "event": "task.done", "created_at": "2026-10-27T08:01:02Z",
 "data": {"plant_id": "p_123", "plant": "Monstera", "task": "water", "days_late": 1, "completed_at": "…"}}

The data of tasks.due has date, tasks (plant_id, plant, task, due, overdue_days) and reminders. The headers are X-PlantJournal-Event, User-Agent: PlantJournal-Webhook/1 and X-PlantJournal-Signature: t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256, with your signing secret as the key, of the text <t>.<raw body>. Check it, and reject a t that is more than a few minutes old:

const sig = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(v1));

Answer with any 2xx status within 5 seconds. Otherwise the message is tried again after 1 minute and after 5 more minutes (the same message, a new signature time), so make your handler safe to run twice (the id is the same). After 10 failed messages in a row the webhook is paused until you remove it and add it again. At most 500 messages a day per webhook. The address must be https on a public website name (no IP numbers, no port, no password); what your server answers is not stored. Settings shows the latest messages and whether they arrived.

Limits

WhatLimit
One key60 calls a minute and 5000 a day
A calendar link30 fetches an hour and 200 a day
Changes through keys, per household500 a day
Wrong keys from one address20 a minute, then that address waits a minute
Keys per household5 working keys, 10 new keys a day

Over a limit the answer is 429 with a Retry-After header (seconds).

Answers and errors

Errors look like {"error": {"code": "invalid_key", "message": "…"}}. The codes: invalid_key (401: missing, wrong or revoked key), wrong_key_kind and key_in_url (403), not_found / plant_not_found / reminder_not_found (404), bad_request / bad_json (400), plant_archived, not_during_cutting, task_not_in_use, task_switched_off (409), rate_limited, too_many_failures, household_write_limit (429), api_off (503: the owner switched access keys off), api_admin_only (403: access keys are limited to the owner for now).

What is recorded

Every change made through a key, and every refused or slowed-down call, is recorded with the key's name for 30 days. The household sees it in Settings. Plain reads are not recorded; the key shows when it was last used. The privacy policy has the details.