Compare Terra Webhook Payloads
Terra sends your webhook the same night or the same day more than once, and the second delivery doesn't match the first: a field that was null has a value, a total has grown. Slicelytics puts two payloads side by side and lists every field that changed, so you can see which delivery to keep and what your code missed. The payloads stay on your computer.
See What the Second Delivery Added

The sample is two sleep events for one user connected through Garmin, both for the night of 20 to 21 October, delivered at 07:03 and 07:49 local time. Both have the same metadata.summary_id, so the second is an update of the first. Turn on Expand in the Diff view to see the three fields that changed:
heart_rate_data.summary.avg_hrv_rmssdwent fromnullto 44. Terra's docs say that for Garmin it comes from Garmin's overnight HRV summary, which Garmin sends separately from the sleep stages.heart_rate_data.summary.resting_hr_bpmwent fromnullto 49. For Garmin, Terra copies it from the daily summary of the day the user woke up, so it stays empty until that summary has arrived.user.last_webhook_updatemoved from 05:03:27 to 05:49:52 UTC.
Everything else is the same in both: the stage durations, the 20 hypnogram samples, the 458 heart rate samples and scores.sleep. A handler that kept the first delivery and dropped the second would have no HRV and no resting heart rate for that night. Both payloads are synthetic, written to Terra's sleep data model. The sample opens without an account.
Which Delivery Replaces Which
Terra's docs say to always overwrite a record with the latest payload, and name the field that identifies the record for each event type:
| Event type | Overwrite by |
|---|---|
activity, sleep | metadata.summary_id. A sleep can be re-sent with a later end_time but the same summary_id, so don't match sleeps by their start and end times. |
daily, body | metadata.start_time, the day's midnight. A daily payload is a running summary of the day, sent several times with the totals so far. |
Apple Health is the exception: its sleeps have no ID of their own, so Terra generates summary_id from the start time, and a night whose start moves comes back with a new one. Terra recommends matching Apple Health nights by overlapping start and end times.
Save Every Delivery
Terra signs each webhook with a terra-signature header, computed over the raw body, so your handler already has the body as it arrived. Write it to a file before you parse it, one file per delivery or one line per delivery in an .ndjson log. When a number in your database looks wrong, open two deliveries of the same record in Compare.
A log of deliveries opens in Slicelytics too, however large: filter it by type, by user.user_id or by data.metadata.summary_id to find every delivery of one record. See View JSONL Logs.
Compare Two Providers, or Two Schema Versions
Terra normalises every provider to the same data model, so a Garmin night and an Oura night of the same user line up field by field in Compare, where the providers' own responses wouldn't. Fields one provider doesn't fill are null on its side, which makes it a quick way to see what you lose or gain when a user switches devices.
Terra also adds fields to its payloads without announcing them in advance, arriving as null until they are computed. Open a payload you stored months ago next to a new one of the same type, and the Diff view lists the new keys before a strict parser trips on them.
To compare the providers' own responses, see Compare Oura API Responses, Compare WHOOP API Responses and Compare Garmin Health API Data, or the wearable API guide for any provider.
Common Questions
How do I know a payload is the final version?
Terra's docs describe no final flag. They say provider data is eventually consistent, that sleep and readiness can be finalised hours later and daily totals keep growing, so you'll get several webhooks for the same record. Overwrite with the latest each time, and compare deliveries when a value looks wrong.
Why is avg_hrv_sdnn always null for Garmin?
Terra's docs say it is always empty for Garmin. Garmin's overnight HRV is RMSSD, which arrives as avg_hrv_rmssd once Garmin has sent its HRV summary for the night.
Can I compare data I fetched from Terra's API instead?
Yes. A response from an endpoint such as /v2/sleep or /v2/daily, requested with to_webhook=false, is JSON too and opens the same way. You can also compare what you fetched with what a webhook delivered for the same record.
Can I compare a daily and a sleep payload?
You can open them, but they share only a few fields, such as metadata and device_data. Compare two payloads of the same event type, ideally of the same record: the same summary_id, or the same start_time for days.
Does the data leave my computer?
No. Slicelytics reads the files in your browser and uploads nothing. If you share a comparison, the data travels in the part of the link after #, which browsers never send to a server, so treat the link like the data.