Skip to main content

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[].startDate
  • sessions[].endDate
  • externalReviews[].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[].dueAt is paired with deadlines[].timeZone
  • discountStartsAt and discountEndsAt share discountTimeZone
{
"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, and archivedAt
  • Nested createdAt values (sessions, deadlines, and other children)
  • Webhook occurredAt

Continue to treat these as instants.