A meditation app, a physio's booking system, a habit tracker, a school's timetable — each can push a person's activity straight into their TimePeace diary, mapped onto the diary types they already use, visibly attributed to you. You build to one endpoint; every person who connects your app makes both products more useful.
And because a person's real data is end-to-end encrypted and unreadable to our servers, your integration lives entirely outside that boundary — which is exactly why it can only ever touch what it put there.
The API is a write-in rail. It never returns a person's diary data to you — not yours, not anyone's. There is no read-a-user endpoint, by design.
Every item you push is stamped with your app's id and your own id for it. A delete can only ever tombstone your own rows — never another app's, and never a person's hand-typed entries.
Two independent keys are required on every request: your app key proves you're a trusted app; the grant token proves this particular person let you in. A valid app key with no grant can address nobody.
One endpoint, POST only. The action is named in the JSON body.
POST https://lhcsqadnkjqbonhmvqcn.supabase.co/functions/v1/inbound-ingest
Send three headers. Your app key and the person's grant token are your credentials; the apikey is our public project key (safe to embed — it identifies the project, not you).
apikey: sb_publishable_s7EFgzZ3PVzjr8sA73GJvA Authorization: Bearer tp_live_your_app_key x-tp-grant: the_persons_grant_token Content-Type: application/json
Both credentials are checked on every call. A missing or wrong key returns 401 unauthorized; a suspended app or a revoked grant is treated the same way — no detail is leaked about which failed.
| Action | Does |
|---|---|
push | Stage one or many items into the person's diary (idempotent). |
delete | Remove items you previously added, by your own external id. |
list_mine | List the items you have staged for this person, so you can reconcile. Returns only your own rows — never the person's data. |
status | Health check: confirms your keys work and echoes the scopes the person granted you. |
Send up to 100 items. Each item carries your own external_id (used for idempotency and for later deletion), a kind, an optional occurred_at (ISO 8601), and a payload whose fields must match that kind.
{
"action": "push",
"items": [
{
"external_id": "session-8412",
"kind": "session",
"occurred_at": "2026-07-02T07:30:00Z",
"payload": { "title": "Morning meditation", "duration_min": 15, "category": "mindfulness" }
}
]
}
Response — every item reports its own result, and nothing is dropped silently:
{ "ok": true, "action": "push", "staged": 1, "failed": 0,
"results": [ { "external_id": "session-8412", "ok": true, "result": "created" } ] }
A repeat push of the same external_id updates the item ("result":"updated") rather than duplicating it. Send an idempotency_key on an item and an exact retry is a no-op ("result":"duplicate"). If the person has deleted an item themselves, re-pushing it will not resurrect it ("result":"dismissed").
Name one or many of your own external_ids (up to 200). Only your rows are affected.
{ "action": "delete", "external_id": [ "session-8412", "session-8399" ] }
{ "ok": true, "action": "delete", "tombstoned": [ "session-8412", "session-8399" ] }
{ "action": "list_mine", "status": "pending", "limit": 200 }
Returns your staged items with their status (pending · merged · tombstoned) and whether the person dismissed each — enough to reconcile your side, and nothing of theirs.
{ "action": "status" }
{ "ok": true, "action": "status", "app": "your-slug", "live": true, "scopes": ["health"] }
Each kind maps to a diary type TimePeace already understands, so imported data renders natively with no new interface. A payload may carry the common fields — title, name, note, duration_min — plus that kind's own fields below. Any unknown field is rejected, so you always know exactly what was accepted.
| kind | Becomes | Scope | Payload fields |
|---|---|---|---|
workout | a workout | health | sport distance_km calories heart_rate_avg steps |
sleep | a sleep record | health | start end quality stages |
meal | a meal | baseline | calories items meal_type |
activity | a hobby / activity | baseline | category |
session | a hobby / activity | baseline | category |
appointment | an appointment | baseline | location with start end |
booking | an appointment | baseline | location with start end reference |
milestone | a milestone | baseline | value unit |
study_session | a study session | baseline | subject course |
event | a general event | baseline | category location |
A health kind is accepted only when the person granted your app the health scope and holds their own health permission — belt and braces. baseline kinds need only an active grant.
Up to 100 items per push, 200 ids per delete, an 8 KB payload ceiling per item, and an occurred_at that may lead now by at most a day and trail it by at most about five years (no backdated floods). Rate limits apply per app and per (app, person); over the limit you get 429 with a Retry-After header.
Items are keyed by (your app, the person, your external_id). Re-sending the same external id updates in place; it never duplicates. If a person's workout also arrives through their own health connection, TimePeace de-duplicates so it isn't counted twice.
| HTTP | Meaning |
|---|---|
401 | Missing or invalid app key or grant token (or the app is suspended / grant revoked). |
400 | Bad request — no_items, no_ids, or bad_action. |
413 | too_many_items / too_many_ids — you exceeded a batch cap. |
422 | Every item in a push failed validation (see per-item errors). |
429 | Rate limited — retry after the Retry-After seconds. |
503 | not_enabled — the rail is not yet live (the preview state of this page). |
Per-item push errors include unknown_kind, scope_denied, unknown_field:<name>, payload_too_large, bad_occurred_at, and occurred_at_out_of_range. Each is returned against its own external_id so a bad item never sinks a good batch.
A person's real diary is end-to-end encrypted. Our servers hold only ciphertext — they cannot read or write a single diary entry. So your integration never touches that store at all. Instead it writes to a small, provenance-stamped staging lane, and the person's own device (the only thing holding the key) drains it into their encrypted diary, tagging each item as from your app.
That is why the reversibility rule is structural. Your delete can only address rows stamped with your app id. A person's hand-typed entries have no id you could name and no key you could hold — they are simply unreachable from where your integration lives. If a person edits an item you imported, it detaches from you and your later delete leaves their version untouched. Their hand always wins.
For the person, that means connecting your app is safe in a way "give an app access to my data" usually isn't: you can add, you can tidy up after yourself, and you can never see or delete the rest of their life.
Access is approved by hand in this first version, the same way we run our partner programme — a small, deliberate start. Tell us about your app and what you'd feed in; we'll review and, on approval, issue your API key (shown once) so you can start pushing.
We've received your application. We review your app and homepage, then reply by email with next steps.