Dates and Timezones
The Data API uses three temporal shapes. Mixing them up is the usual way a calendar day shifts by one when a client formats it in a local timezone.
Calendar dates
These fields are YYYY-MM-DD strings (format: date) with no time and no
timezone:
sessions[].startDatesessions[].endDateexternalReviews[].date
Treat them as calendar values. Do not call new Date(value) and then render in
the client's local timezone. A value such as 2027-07-11 is midnight UTC in
many JavaScript runtimes, so toLocaleDateString in a US timezone shows July
10. Prefer a date-only parser, or split the string into year, month, and day
for display.
A session spanning 2027-07-11 through 2027-07-17 must display those exact
days in every client timezone.
Deadlines and discount windows
Deadline and discount values are instants: offset-bearing ISO 8601 strings paired with an IANA timezone.
deadlines[].dueAtis paired withdeadlines[].timeZonediscountStartsAtanddiscountEndsAtsharediscountTimeZone
{
"dueAt": "2027-07-11T23:59:59.999-04:00",
"timeZone": "America/New_York"
}
The numeric offset identifies the instant. The IANA zone is the advertised local zone and carries daylight-saving rules, so a window can have different offsets at its start and end. Use the offset ISO string for comparisons and conversion to the viewer's local time; use the IANA field when showing the advertised wall clock or a zone label.
Cutoffs are recorded to the minute. An inclusive cutoff — dueAt and
discountEndsAt — carries the last instant of its minute (:59.999), while
discountStartsAt carries the first (:00.000). Compare against these values
directly rather than rounding them.
When a deadline has no cutoff (for example a rolling deadline), both dueAt and
timeZone are null. They are never mixed: an instant always ships with a
valid IANA name, and a missing instant always ships with a null zone.
The same pairing holds for discounts: discountTimeZone is null when both
discountStartsAt and discountEndsAt are null. An open-ended window with
only one bound still includes the timezone.
On GET /v1/learning-opportunities and
GET /v1/learning-opportunities/{id}, the Data API masks every discount field
outside an active window, including discountTimeZone. The sync endpoints
under /v1/sync/ always include the stored discount bounds and
discountTimeZone so a consumer can evaluate the window itself.
Webhook snapshots use the same display resource as
GET /v1/learning-opportunities/{id}, so they follow display masking.
Audit and event timestamps
Creation, update, archive, and webhook timestamps remain ordinary ISO 8601 instants in UTC. They do not gain an IANA field.
- Learning opportunity
createdAt,updatedAt, andarchivedAt - Nested
createdAtvalues (sessions, deadlines, and other children) - Webhook
occurredAt
Continue to treat these as instants.