# Time in the API

Instants are RFC 3339 in UTC with a `Z`, as in `2026-09-10T09:15:00Z`. The webhook signing header is the exception: Unix seconds as a string, because it is compared arithmetically rather than displayed. Verification dates are plain calendar dates. Local time matters in exactly one place — when a person reads a due time or a business-hours window.

- **Status:** Available
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/developers/time-and-timezones/

## Every time value on the public surface

| Field | Where | Format |
|---|---|---|
| `occurredAt` | Event envelope | RFC 3339 date-time, UTC: `2026-09-10T09:15:00Z` |
| `dueAt` and similar inside `data` | Event envelope | The same RFC 3339 form |
| `X-Connect-Timestamp` | Webhook header | Unix seconds, as a decimal string: `1789045200` |
| `updated` | Page record | Calendar date: `2026-09-10` — no time, no zone |
| `generated` | Manifests | Calendar date, the corpus verification date |
| `created_at`, `last_used_at`, `revoked_at` | Integration key description | ISO 8601 with a trailing `Z` |

Two formats rather than one is deliberate. `occurredAt` is read by people and by code and benefits from being unambiguous on sight. The signing header is only ever subtracted from the current time, and Unix seconds make that a single comparison with no parser in the path — which matters when it is the check standing between you and a replayed request.

## Why dates are dates and not timestamps

`updated` and `generated` carry no time of day, and that is honest rather than lossy. They record the day a page's facts were verified, and verification is a piece of work done on a day, not an event that happened at 14:32:07. A timestamp there would imply a precision nobody has.

The practical consequence: compare them as dates. Parsing `2026-09-10` into a datetime at midnight and then converting it to a local zone will move it to the previous day for anyone west of UTC, which turns 'verified today' into 'verified yesterday' for half your users.

> **Note** `generated` is the same value across every manifest in one build, so it is a build stamp. Pair it with `version` to detect a rebuild; use per-page `updated` to detect that a particular page's facts were re-checked.

## Clocks, windows and skew

Three time windows appear on the public surface, and they behave differently on purpose.

**The signature freshness window** — Reject an `X-Connect-Timestamp` far from now — a few minutes is the guidance. This is the only place where *your* clock being wrong causes valid requests to be rejected, which is a good argument for running NTP on a webhook receiver.
**The rate-limit window** — Sixty seconds, measured on a monotonic clock rather than the wall clock. A daylight-saving change or an NTP correction cannot move it, and cannot accidentally grant or withdraw a caller's allowance.
**The cache window** — `max-age=300`. Five minutes of a previous build being served is five minutes of something that was true; the corpus changes on a deploy, not on a request.

None of these needs your timezone. Everything published is UTC or zone-free, so a client that stores UTC and converts only at the point of display never has a conversion bug to find.

## The one place local time is the truth

When a person reads a time, the business's own timezone is the correct one — a follow-up due 'tomorrow morning' means morning where the business is, and business hours are a local-clock concept by definition. A call that arrives outside them is outside them locally, not at some UTC offset.

That is a presentation and scheduling concern inside the workspace rather than a wire-format one. The wire stays UTC; the interpretation is local. Keeping those apart is what stops a follow-up from drifting an hour twice a year.

So in your client: store the instant, render in the reader's zone, and do not attempt to re-derive a business's local intent from a UTC value alone. The instant is a fact; the intent belongs to the workspace that set it.

## Questions

### Can I rely on `occurredAt` being unique per event?

No. Two events can share an instant. `id` is the identity; `occurredAt` is when it happened, and it is the right field to order by — with `id` as a tiebreak if you need a total order.

### What if my clock is ahead of Connect's?

A moderate skew is absorbed by the few-minute freshness window. A large one makes every signed request look stale and every verification fail, which is why the symptom of a broken receiver clock is 'everything is rejected' rather than an intermittent fault.

### Does the API accept a timezone parameter?

There is nothing to parametrise: the public routes are documentation reads and return dates and instants as published. Conversion belongs in your client.

## Related

- [Verifying a Connect webhook signature](https://connectbyjbrh.com/developers/webhook-signatures/)
- [Stable identifiers](https://connectbyjbrh.com/developers/entity-ids/)
- [The machine-readable documentation](https://connectbyjbrh.com/developers/machine-manifests/)
- [Designing a resilient client](https://connectbyjbrh.com/developers/errors-and-retries/)
- [Scheduling formats](https://connectbyjbrh.com/docs/protocols/ical-scheduling/)

## What this page is based on

- `webapp/developers/asyncapi.yaml` — `occurredAt`, `X-Connect-Timestamp`
- `backend/app/public_developer_api.py` — the rate window and cache header
- `tools/docs_build.py` — `generated` from the corpus verification date
- `backend/app/integration_auth.py` — `describe()` timestamp rendering
