TempoLife

TempoLifeFeaturesAutomations and webhooks

Automations and webhooks

The event catalogue, the exact JSON payloads, HMAC-SHA256 request signing, the retry policy and how to register a webhook. Registration is open; deliveries begin when the worker ships.

No deliveries are being sent. The delivery worker is not built, so a webhook registered today records your URL, your event and your signing secret and then sits there — TempoLife makes no outbound request to it, not even a test ping. Registering now is only worth doing if you want the secret ahead of time so you can write and unit-test your verification code. Deliveries begin when the worker ships.

4triggers
0 builtactions
HMAC-SHA256signature
300 sclock tolerance

Triggers

Volumes are per user, per day, for an actively logging account.
EventNameWhen it firesExpected volume
meal.loggedMeal loggedFires once for every meal saved, however it was entered — photo, search or a repeat of yesterday.Three to six a day for an active user; the noisiest event here.
weight.updatedWeight updatedFires on a new weigh-in, whether typed in or read from Health Connect.Zero to one a day. The quietest useful event.
steps.goal_reachedStep goal reachedFires the first time the day's step total crosses the user's own goal. Once per day at most.At most one a day, and never twice for the same date.
fast.completedFast completedFires when a fasting window ends, with the planned and actual duration.Zero to one a day, only for users who fast.
test.pingTest pingSent on demand from the webhook list, so you can verify your endpoint and your signature check.Only when you ask for it.

Actions — specified, not built

The other half of an automation platform is the ability to write back. TempoLife cannot do that from outside the app: there is no public write endpoint anywhere in the product. Two actions are specified so the design is on record, and both are blocked on the same missing piece.

ActionNameWhat it would doStatus
water.logLog waterAdd a volume in millilitres to today's hydration total.not built
weight.logLog weightRecord a weigh-in for a given date, in grams.not built

What a payload looks like

Every delivery is a POST with Content-Type: application/json; charset=utf-8 and a body in this shape. Fields are additive: new keys may appear inside data without warning, so parse defensively and ignore what you do not recognise.

{
    "id": "evt_01J8W3M7Y4A2B6",
    "type": "meal.logged",
    "created": "2026-09-02T12:31:04Z",
    "api_version": "2026-09-02",
    "data": {
        "meal_id": 918273,
        "logged_at": "2026-09-02T12:30:41Z",
        "meal_type": "lunch",
        "name": "Chicken breast and rice",
        "portion_g": 340,
        "nutrition": {
            "kcal": 512,
            "protein_g": 46.2,
            "carbs_g": 58.1,
            "fat_g": 8.4
        },
        "day_totals": {
            "kcal": 1487,
            "protein_g": 96.4,
            "target_kcal": 2100
        }
    }
}

The envelope is the same for every event: a unique id you should use for idempotency, the type, a UTC created timestamp, an api_version dated to the day the payload shape was fixed, and the event-specific data. Two more examples:

weight.updated

{
    "id": "evt_01J8W3Q5F0M2K7",
    "type": "weight.updated",
    "created": "2026-09-02T07:14:09Z",
    "api_version": "2026-09-02",
    "data": {
        "date": "2026-09-02",
        "weight_g": 78400,
        "previous_weight_g": 78900,
        "change_g": -500,
        "source": "health_connect"
    }
}

steps.goal_reached

{
    "id": "evt_01J8W3R1XB4C9D",
    "type": "steps.goal_reached",
    "created": "2026-09-02T18:41:55Z",
    "api_version": "2026-09-02",
    "data": {
        "date": "2026-09-02",
        "steps": 10214,
        "goal": 10000,
        "reached_at": "2026-09-02T18:41:52Z",
        "source": "health_connect"
    }
}

fast.completed

{
    "id": "evt_01J8W3S8P2N6H1",
    "type": "fast.completed",
    "created": "2026-09-02T12:00:31Z",
    "api_version": "2026-09-02",
    "data": {
        "plan": "16:8",
        "started_at": "2026-09-01T20:00:00Z",
        "ended_at": "2026-09-02T12:00:00Z",
        "planned_minutes": 960,
        "actual_minutes": 960,
        "completed": true
    }
}

Signing

Every delivery carries a X-TempoLife-Signature header. It holds a UNIX timestamp and a hex HMAC-SHA256, computed over the timestamp, a literal dot and the exact raw request body — the bytes as received, before any JSON parsing, re-encoding or whitespace normalisation. Re-serialising the body before you verify is the single most common reason a signature check fails.

X-TempoLife-Signature: t=1788334264,v1=9f8c…64 hex chars…

signed_payload = t + "." + raw_body
signature      = hex( HMAC-SHA256(secret, signed_payload) )

Three rules for the receiving side. Compare in constant time — hash_equals in PHP, crypto.timingSafeEqual in Node — because a byte-by-byte comparison leaks the answer. Reject a timestamp more than 300 seconds from your own clock, which is what stops a captured request being replayed at leisure. And reject before you act, not after: verify the signature as the first thing your handler does.

Verifying in PHP

<?php
$raw    = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_TEMPOLIFE_SIGNATURE'] ?? '';
$secret = getenv('TEMPOLIFE_WEBHOOK_SECRET');

if (!preg_match('/(?:^|,)\s*t=(\d+)/', $header, $mt)
 || !preg_match('/(?:^|,)\s*v1=([0-9a-f]{64})/i', $header, $mv)) {
    http_response_code(400); exit;
}
if (abs(time() - (int)$mt[1]) > 300) { http_response_code(400); exit; }   // replay

$expected = hash_hmac('sha256', $mt[1] . '.' . $raw, $secret);
if (!hash_equals($expected, strtolower($mv[1]))) { http_response_code(401); exit; }

$event = json_decode($raw, true);
// Idempotency: $event['id'] is unique per delivery, and a retry reuses it.
http_response_code(200);

Verifying in Node

import crypto from 'node:crypto';

// Mount with a raw body parser: express.raw({ type: 'application/json' })
export function handler(req, res) {
  const header = req.get('X-TempoLife-Signature') || '';
  const t  = /(?:^|,)\s*t=(\d+)/.exec(header)?.[1];
  const v1 = /(?:^|,)\s*v1=([0-9a-f]{64})/i.exec(header)?.[1];
  if (!t || !v1) return res.sendStatus(400);
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);

  const expected = crypto.createHmac('sha256', process.env.TEMPOLIFE_WEBHOOK_SECRET)
    .update(t + '.' + req.body)          // req.body is a Buffer, not an object
    .digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(v1.toLowerCase(), 'utf8');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  res.sendStatus(200);                    // acknowledge fast, work afterwards
}

Retries and failure handling

The published policy, which the worker will implement:

After the last attempt the delivery is abandoned and the webhook's failure counter increases.
AttemptDelay after the previous tryCumulative age
1immediate0
21 minute1 minute
35 minutes6 minutes
430 minutes36 minutes
52 hours2 h 36 min
6 (last)6 hours8 h 36 min

A delivery counts as successful on any 2xx. Anything else — 3xx included, because a webhook endpoint should not redirect — is a failure, as is a connection that does not complete within 10 seconds. Return your 2xx before you do the work: an endpoint that takes eleven seconds to process a meal will be retried even though it succeeded, and you will double-count. Twenty consecutive failed deliveries disable the webhook so that a decommissioned endpoint does not get hammered forever. Each attempt is recorded with its status code, its duration and any error, so the delivery log can answer "did you send it" precisely.

Because retries exist, duplicate deliveries exist. Deduplicate on the envelope id, which is stable across every attempt at the same event. Storing the last few thousand ids is enough.

How to test before deliveries exist

You cannot receive a real delivery today, so test the half that is in your hands. Generate a signature yourself with your secret and post it at your own endpoint — if your handler accepts that and rejects a tampered copy, it will accept the real thing.

SECRET='whsec_your_secret_here'
BODY='{"id":"evt_test","type":"test.ping","data":{"message":"hello"}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)

curl -sS -X POST https://your-endpoint.example/tempolife \
  -H 'Content-Type: application/json' \
  -H "X-TempoLife-Signature: t=$T,v1=$SIG" \
  --data-raw "$BODY"

Then break it on purpose. Change one character of the body and confirm you get a 401. Subtract an hour from $T and confirm you get a 400. A verification routine that has never rejected anything has not been tested.

Where deliveries would come from

Worth knowing before you write a firewall rule: deliveries will originate from the TempoLife server, not from the user's device, so they arrive from one address rather than many. Requests will carry a recognisable user agent alongside the signature header. Do not allowlist by IP alone — verify the signature, which is the only check that survives an address change.

Register a webhook

Webhooks belong to an account. Sign in to register one — everything else on this page is public and does not need an account.

Sign inCreate an account

Platform guides

Raw webhooks

Point an event at code you control, verify the signature and do whatever you like.

Your own endpoint

Zapier

What exists on Zapier today, and the route that will work when deliveries start.

No TempoLife app published

Make

What exists on Make today, and the route that will work when deliveries start.

No TempoLife module published

IFTTT

What exists on IFTTT today, and the route that will work when deliveries start.

No TempoLife service published

Frequently asked questions

Will my webhook receive anything today?

No. There is no delivery worker, so no HTTP request is made to any registered URL — not even a test ping. The row is stored and the secret is issued so you can write your verification code ahead of time.

Is there a TempoLife app on Zapier, Make or IFTTT?

No, on all three. When deliveries start, the route on each platform is its generic incoming-webhook step: Webhooks by Zapier, a Make custom webhook, or an IFTTT applet fed through their webhooks service.

How do I verify a signature?

Recompute HMAC-SHA256 over the timestamp, a dot and the exact raw request body using your webhook secret, then compare in constant time. Reject anything whose timestamp is more than five minutes from your clock.

Can an automation write into TempoLife?

Not today. The action side of the catalogue is a specification. TempoLife has no public write endpoint at all, so nothing outside the app can log a meal, a weight or a glass of water.

What happens if my endpoint is down?

Under the published policy, five attempts with growing backoff over roughly eight hours, then the delivery is abandoned and the failure counter goes up. Twenty consecutive failures disables the webhook.

Source: TempoLife platform specification — event, signature and retry policy · checked 2026-09-02

Developer API · Device integrations · Sign in with TempoLife

The data has to exist before it can fire an event

Every trigger on this page starts with something logged in the app: a meal, a weigh-in, a step goal, a fast that finished.

Create a free accountGet the app