# Timezone-Aware Booking API: Built for AI Agents & Custom Frontends

> AI agents need precise timezone handling for cross-border bookings. See how Timerise GraphQL API uses UTC-first storage, IANA identifiers, and DateTimeISO.

Page: https://timerise.ai/blog/timezone-handling-in-booking-apis

Your AI agent just booked a consultation for 3 PM. The customer is in Tokyo, the therapist is in Berlin. Which 3 PM did the agent mean?

If the answer isn't instantly clear from the data the API returned, you have a timezone bug - the kind that silently costs businesses thousands in [no-shows](/blog/reducing-no-shows-secure-bookings-and-minimize-cancellations), support tickets, and lost trust. And when **[autonomous AI agents](/blog/ai-agents-booking-what-it-means-for-your-business)** handle bookings without a human double-checking, timezone precision isn't a nice-to-have. It's critical infrastructure.

This is where [GraphQL shines](/blog/graphql-the-perfect-backend-for-ai-agents-and-llm-apps). Unlike REST endpoints that return opaque timestamp strings and leave timezone interpretation to the caller, a well-typed GraphQL API can encode timezone semantics directly into the schema - giving AI agents unambiguous, machine-readable time data in every response.

At Timerise, we've spent years engineering timezone handling that eliminates this entire category of bugs - for human users _and_ for AI agents. Here's how our GraphQL API does it, and what it means for developers building custom frontends or connecting autonomous agents.

## The Core Problem: Local Time Is a Lie (and AI Agents Can't Guess)

Every developer eventually learns this the hard way: **local time doesn't exist in isolation**. "3:00 PM" means nothing without a timezone anchor. A human might infer the right timezone from context. An AI agent can't - it needs explicit, structured data. And timezone handling is far more complex than adding or subtracting hours from UTC.

Consider these complications:

- **Daylight Saving Time (DST)** shifts clocks forward or backward, meaning some local times happen twice a year and some don't exist at all
- **IANA timezone rules change** - governments modify DST rules, sometimes with only weeks of notice
- **UTC offsets aren't static** - `Europe/Warsaw` is `+01:00` in winter and `+02:00` in summer

If your booking system stores times as "3:00 PM CET," you're already in trouble. CET is ambiguous - it doesn't tell you whether DST applies. That's why Timerise follows a different approach entirely.

## Timerise's UTC-First Architecture

Our API follows a strict **UTC-first storage model**. Every timestamp in the database is stored as a UTC value. No local times in storage, no ambiguity, no DST surprises waiting to happen.

We pair this with **IANA timezone identifiers** - the industry standard maintained by the [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Instead of vague abbreviations like "EST" or "CET," we use precise identifiers like `America/New_York` or `Europe/Warsaw` that encode the complete history of timezone rules for a given location.

Every project in Timerise carries a required `localTimeZone` field - a single, queryable source of truth that AI agents can read programmatically:

```graphql
type Project {
  localTimeZone: TimeZone! # e.g. "Europe/Warsaw"
  now: DateTime! # Current UTC time
  nowISO: DateTimeISO! # Current time in project's timezone
}
```

This single field governs all slot generation, booking output, and display formatting for every service under that project. One source of truth, zero guessing games. An AI agent can query `localTimeZone` once and use it to correctly interpret every timestamp the project returns.

## Two DateTime Types: Machine-Readable and Human-Readable

The Timerise API exposes two scalar types that solve the "which 3 PM?" problem. This distinction is what makes the API exceptionally **AI-agent-friendly** - the agent always knows which field to use for logic and which for display:

| Scalar        | Format               | Example                     | Use Case                              |
| ------------- | -------------------- | --------------------------- | ------------------------------------- |
| `DateTime`    | UTC ISO 8601         | `2026-02-16T19:43:29.000Z`  | Time arithmetic, storage, comparisons |
| `DateTimeISO` | ISO 8601 with offset | `2026-02-16T20:43:29+01:00` | Human-readable display                |

**`DateTime`** is the machine-friendly format. It always ends with `Z` (Zulu time = UTC). AI agents should use this for all time arithmetic - comparisons, sorting, conflict detection, and scheduling logic.

**`DateTimeISO`** is the human-friendly format. It shows the local time _with_ the UTC offset, so a user in Warsaw sees `20:43:29+01:00` - immediately clear, no mental math required. When an AI agent needs to present times to end users, `DateTimeISO` provides a ready-to-display value with the correct local offset baked in.

Every slot in the system exposes both:

```graphql
type Slot {
  dateTimeFrom: DateTime! # "2026-02-16T19:43:29.000Z"
  dateTimeFromISO: DateTimeISO! # "2026-02-16T20:43:29+01:00"
  dateTimeTo: DateTime!
  dateTimeToISO: DateTimeISO!
}
```

**For developers building custom frontends**, the rule is straightforward: use `dateTimeFrom` / `dateTimeTo` for any time arithmetic, and `dateTimeFromISO` / `dateTimeToISO` for rendering to users.

## Auto-Detecting the User's Timezone in Custom Frontends

When you build a custom booking page on top of Timerise, your users might be in a different timezone than the service provider. Here's how to handle this gracefully.

### Browser-Side Detection

Modern browsers expose the user's timezone through the `Intl` API (Application Programming Interface):

```javascript
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Returns IANA identifier, e.g. "America/New_York"
```

This returns a proper IANA identifier - exactly the format Timerise expects. You can pass it directly to the API when creating bookings:

```graphql
mutation CreateBooking {
  bookingCreate(
    serviceId: "srv_12345"
    slots: ["slot_abc"]
    timeZone: "America/New_York" # from Intl API
  ) {
    bookingId
    dateTimeFrom
    dateTimeFromISO
  }
}
```

The `timeZone` parameter gets stored on the booking document. Later, when the user views their confirmation, the `dateTimeFromISO` field automatically formats the time in _their_ timezone with the correct offset.

### Recalculating Display Times for Users

What if your frontend needs to show slot times in the user's local timezone rather than the provider's? The API returns both UTC and provider-local times, giving you full flexibility.

**Option A - Let the API handle it.** Query slots normally and use the `DateTime` (UTC) fields. Then format them client-side using the detected timezone:

```javascript
const slotUTC = "2026-02-16T19:43:29.000Z";
const userTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;

const localTime = new Date(slotUTC).toLocaleString("en-US", {
  timeZone: userTZ,
  hour: "2-digit",
  minute: "2-digit",
  hour12: true,
});
// "2:43 PM" for America/New_York (UTC-5)
```

**Option B - Show both perspectives.** Display the provider's local time (`dateTimeFromISO`) alongside the user's local time. This builds trust in cross-timezone bookings:

> **Session Time:** 8:43 PM Warsaw (2:43 PM your time)

Both approaches work because the API gives you the raw UTC data and the provider-formatted local time. You choose how to present it.

## Slot Generation: Where Timezones Get Tricky

Behind the scenes, Timerise's slot generation engine handles the hardest timezone problems so you don't have to.

When a service provider sets their availability as "Monday through Friday, 9:00 AM to 5:00 PM," they mean **local time**. But the system stores everything in UTC. The conversion has to account for DST transitions, where the offset changes mid-schedule.

Timerise solves this with the [`date-fns-tz`](https://github.com/marnusw/date-fns-tz) library and three core functions:

- **`fromZonedTime(localDate, timeZone)`** - converts wall-clock time to UTC
- **`toZonedTime(utcDate, timeZone)`** - converts UTC to wall-clock time
- **`formatInTimeZone(utcDate, timeZone, format)`** - formats a UTC date for display in any timezone

The generation pipeline works like this:

1. Provider's schedule (local times) → converted to UTC boundaries via `fromZonedTime`
2. Slot overlap detection → pure UTC millisecond arithmetic, no timezone conversions
3. Working hours filtering → `toZonedTime` shifts back to local for time-of-day comparisons
4. Output assembly → both `DateTime` (UTC) and `DateTimeISO` (offset-formatted) fields

This means a 9:00 AM slot in `Europe/Warsaw` correctly becomes `08:00:00Z` in winter (`+01:00`) and `07:00:00Z` in summer (`+02:00`). The slot generation handles this automatically across DST boundaries - no manual intervention needed.

## Practical Tips for Frontend Developers

Building a custom frontend on Timerise? Keep these guidelines in mind:

### 1. Always Send UTC for DateTime Inputs

The `DateTime` scalar parses inputs as UTC. Don't send local times without a `Z` suffix - they'll be misinterpreted:

```
✅ Correct: "2026-02-16T19:43:29.000Z"
❌ Wrong:   "2026-02-16T20:43:29"  (treated as UTC, off by 1 hour)
```

### 2. Know When to Use `DateTimeISO` vs `DateTime` Inputs

`DateTimeISO` output fields are computed at query time from the stored UTC value and the booking's timezone. But `DateTimeISO` is also accepted as an **input** - `bookingCreate`, `bookingReschedule`, `bookings`, and `services` all support `dateTimeISOFrom` / `dateTimeISOTo` parameters. Use `DateTimeISO` inputs when you already have a local time with offset (e.g., from a user's calendar). Use `DateTime` (UTC) inputs when doing time arithmetic or when you've already converted to UTC.

### 3. Handle Missing Timezone Gracefully

Timezone mismatches are one of the most [common booking API issues](/blog/troubleshooting-booking-api-issues), and missing data is where they start.

Older booking records might lack a `timeZone` field, causing `dateTimeFromISO` to return `null`. Always fall back to the UTC `dateTimeFrom` field:

```javascript
const displayTime = booking.dateTimeFromISO ?? new Date(booking.dateTimeFrom).toLocaleString();
```

### 4. Detect Timezone Once, Reuse Everywhere

Call `Intl.DateTimeFormat().resolvedOptions().timeZone` once on page load and store it. Pass the same IANA identifier to every `bookingCreate` or `bookingReschedule` mutation. Consistency prevents mismatched timezone records. For AI agents, the timezone should be captured from the user's profile or session context and passed consistently through every API call.

### 5. Remember: DST Days Aren't Always 24 Hours

If you're building a day-view calendar, don't assume `24 * 60 * 60 * 1000` milliseconds equals one day. During DST transitions, a local day can be 23 or 25 hours. Use library functions (like `date-fns`) for date arithmetic instead of raw millisecond math.

## DateTimeISO as Input: Send Local Times Directly

Beyond being an output scalar, `DateTimeISO` is accepted as an **input type** for queries and mutations. This means you can send local times with their UTC offset directly - the API parses the offset and converts to UTC internally.

This is a significant improvement for **AI agents and LLM-powered apps**. Instead of converting local times to UTC before every API call, an agent can pass the user's local time _as-is_ with the offset - reducing conversion logic, eliminating a class of bugs, and making the agent's code simpler and more reliable.

The following queries and mutations accept `dateTimeISOFrom` / `dateTimeISOTo` parameters:

- **Queries**: `bookings` and `services` - filter results by local time range
- **Mutations**: `bookingCreate` and `bookingReschedule` - specify booking times in local time

```graphql
mutation CreateBooking {
  bookingCreate(
    serviceId: "srv_12345"
    dateTimeISOFrom: "2026-03-15T09:00:00+01:00"
    dateTimeISOTo: "2026-03-15T10:00:00+01:00"
  ) {
    bookingId
    status
  }
}
```

```graphql
query GetBookings {
  bookings(
    dateTimeISOFrom: "2026-03-15T00:00:00+01:00"
    dateTimeISOTo: "2026-03-16T00:00:00+01:00"
  ) {
    bookingId
    dateTimeFrom
    dateTimeFromISO # local start time with UTC offset (e.g. "2026-03-15T09:00:00+01:00")
  }
}
```

No manual UTC conversion. No offset arithmetic. The API handles it.

## Why GraphQL + Full Timezone Support Is the Best Stack for AI Agents

Timezone bugs aren't edge cases - they're revenue killers. Every wrong appointment time means:

- **No-shows** from confused customers
- **Support tickets** from frustrated users
- **Double bookings** when slots overlap across DST transitions
- **Legal liability** in healthcare or legal scheduling

When AI agents handle bookings autonomously, for example through the [Timerise MCP server](/blog/mcp-server-your-ai-agent-just-learned-to-book), the stakes are even higher. There's no human in the loop to catch a timezone mistake before it reaches the customer. The API _must_ be correct by design.

This is where Timerise's combination of **GraphQL** and **full timezone support** creates a uniquely powerful stack for autonomous agents:

1. **Schema-level type safety** - `DateTime`, `DateTimeISO`, and `TimeZone` scalars prevent the agent from sending malformed time data. The schema is the contract.
2. **Introspection** - an AI agent can query the schema itself to discover available fields and types. No documentation parsing, no hallucination risk.
3. **Precise data fetching** - the agent requests only the time fields it needs. No over-fetching, no wasted tokens.
4. **Unambiguous timezone semantics** - every response includes both UTC and offset-formatted times. The agent always knows exactly what timezone a value represents.
5. **`DateTimeISO` inputs** - agents send local times directly, eliminating client-side UTC conversion entirely.

REST APIs leave timezone interpretation to the caller. GraphQL APIs with typed timezone scalars make it **impossible to misinterpret a timestamp**. For AI agents operating at scale across multiple timezones, this isn't a convenience - it's a requirement.

Timerise's API handles the complexity. Your frontend - or your AI agent - handles the experience. Your users see the right time, every time.

---

**Ready to build timezone-aware booking experiences with AI agents?**

[Test GraphQL queries live →](https://api.timerise.io/graphql)

[Start your project →](/brief)

<br>

[Matt Sowa](https://www.linkedin.com/in/mateusz-sowa/)
