Show opening hours on a business page

Display opening status automatically from maintained local hours and an explicit holiday policy.

JavaScript (browser)Last tested Get the complete example ↓
On this page

Working example

HTML + JavaScript
Use this example

Start with an opening-status display

The business page shows Open until 5 PM, Closed, or Hours unavailable as soon as it loads. Visitors do not have to run a calculator. The fictional New York shop opens Monday–Friday, 09:00–17:00 and closes on US observed federal holidays.

The check combines three things: the business's local time, its holiday calendar, and the hours you maintain. Configure the business once; the page supplies its current status automatically.

Choose Use this example above to prepare the HTML for your website or localhost. The setup provides a public key and local run instructions. Keep secret keys on your server.

1. Configure the business once

Keep the business's maintained schedule in your application. The download defines one sample in BUSINESS; visitors only need its opening status.

JavaScript
const BUSINESS = {
  name: 'Sample shop', city: 'New York',
  location: { zone: 'America/New_York', country: 'US', region: null },
  opens: 9 * 60, closes: 17 * 60, week: 'weekdays', closures: new Set(), holidayPolicy: 'public'
};

The explicit holidayPolicy: 'public' closes this sample on applicable public holidays; 'none' means your business does not use those dates as closures. Missing policy is not guessed. Opening times are minutes after local midnight. 9 * 60 means 09:00. closures can hold additional local dates, such as new Set(['2026-12-24']). Update the displayed holiday-policy text when adapting the location and calendar.

An earlier closing time means an overnight shift. Equal opening and closing times are rejected. Extra closures close the entire local day, including any shift carried from yesterday.

2. Check the business's local date

The initial check captures one UTC instant from the browser clock and converts it with /time/UTC?to={timezone}&at={time}. Its returned local date selects the year for /holiday/{country}?year={year}. Pass those facts to openingStatus with your configured hours:

JavaScript
async function checkBusiness(config, signal) {
  const { location, instant, opens, closes, week, closures } = config;
  if (!location || typeof location.zone !== 'string' || !location.zone || !/^[A-Z]{2}$/.test(location.country) ||
      (location.region !== null && typeof location.region !== 'string') ||
      ![opens, closes].every(value => Number.isInteger(value) && value >= 0 && value < 1440) || opens === closes ||
      !['daily', 'weekdays'].includes(week) || Object.prototype.toString.call(closures) !== '[object Set]' || !['public', 'none'].includes(config.holidayPolicy)) {
    throw new Error('Opening hours need a maintained schedule and explicit holiday policy.');
  }
  for (const date of closures) dateMs(date);
  const data = await getJson('/time/UTC', { to: location.zone, at: instant }, signal);
  if (signal.aborted) throw new Error('The opening-hours check was cancelled.');
  const local = readLocal(data, location.zone, instant);
  const previous = new Date(dateMs(local.date) - DAY).toISOString().slice(0, 10);
  const next = new Date(dateMs(local.date) + DAY).toISOString().slice(0, 10);
  const dates = closes < opens ? local.minutes < closes ? [previous, local.date] : [local.date, next] : [local.date];
  const years = [...new Set(dates.map(date => Number(date.slice(0, 4))))];
  let holidays;
  try {
    const calendars = config.holidayPolicy === 'none' ? [] : await Promise.all(years.map(async year => readCalendar(
      await getJson(`/holiday/${location.country}`, { year: String(year) }, signal), location, year)));
    holidays = new Map(calendars.flatMap(calendar => [...calendar]));
  } catch (error) {
    if (signal.aborted) throw error;
    return { state: 'Unknown', reason: 'The required holiday calendar is unavailable. No opening decision was made.', local, instant };
  }
  return { ...openingStatus(local, { opens, closes, week, closures }, holidays), local, instant };
}

These are pooled Time and Holiday lookups. The ordinary same-day check makes two requests. An overnight shift at New Year can need both years' calendars. The complete source checks the Time response represents the requested instant and filters Holiday entries to applicable public holidays. Observances do not close this sample business.

Closing on these holidays is the sample shop's policy. A required calendar that is missing or malformed produces Hours unavailable.

3. Evaluate the local hours

Check full-day closures first, then the weekly shift. Opening is inclusive and closing is exclusive: the sample shop is open at 09:00 and closed at 17:00 on an ordinary opening day.

JavaScript
function openingStatus(local, config, holidays) {
  const { opens, closes, week, closures } = config;
  const previous = new Date(dateMs(local.date) - DAY).toISOString().slice(0, 10);
  const openingDay = date => week === 'daily' || ![0, 6].includes(new Date(dateMs(date)).getUTCDay());
  const closure = date => closures.has(date) ? 'Your extra closure' : holidays.has(date) ? holidays.get(date).join(' · ') : null;
  const todayClosure = closure(local.date);
  if (todayClosure) return { state: 'Closed', reason: `${local.date}: ${todayClosure}.` };
  let startDay = null;
  if (closes > opens) {
    if (openingDay(local.date) && local.minutes >= opens && local.minutes < closes) startDay = local.date;
  } else {
    if (openingDay(local.date) && local.minutes >= opens) startDay = local.date;
    else if (openingDay(previous) && local.minutes < closes) startDay = previous;
  }
  if (!startDay) return { state: 'Closed', reason: 'Outside the configured weekly opening hours.' };
  const startClosure = closure(startDay);
  if (startClosure) return { state: 'Closed', reason: `The shift's opening date is closed: ${startClosure}.` };
  const next = new Date(dateMs(local.date) + DAY).toISOString().slice(0, 10);
  const until = closes < opens && startDay === local.date && closure(next) ? 0 : closes;
  return { state: 'Open', until, reason: `Open until ${displayTime(until)} local time.` };
}

The helper also supports overnight schedules. A Friday 22:00–02:00 shift can be open at 01:00 Saturday. A closure on Friday prevents the shift from starting; a closure on Saturday blocks the carry. Both occurrences of a repeated clock time receive the same decision under this local-clock policy.

An open result names the closing time. If an overnight shift reaches a full-day closure, it shows Open until midnight. Both years are checked when that boundary crosses New Year.

Each result is a snapshot. The page checks on load; Refresh hours requests a new snapshot. There is no polling. Timeouts and errors show Hours unavailable, and old responses cannot replace a newer check. The update time is visible, with the full timestamp in its accessible title.

For a deterministic test, New York at 2026-09-15T14:00:00Z is open under the sample schedule. At 2026-09-15T21:00:00Z it is closed. Labor Day, September 7, is closed under the stated holiday policy.

Complete example

One HTML file with the markup, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.

Download HTML
View and copy the complete source
business-open-now.html
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Show whether a business is open now</title><style>
    :root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
    @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; } }
    :root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    :root[data-theme="light"] { color-scheme: light; }
    * { box-sizing: border-box; }
    [hidden] { display: none !important; }
    body { margin: 0; padding: 24px; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
    main { max-width: 560px; margin: 0 auto; }
    p { margin: 0; }
    .hint, .note { color: var(--muted); }
    form { margin-top: 0; }
    .fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }
    label { display: block; font-size: 13px; font-weight: 550; }
    input, select, button { font: inherit; }
    input, select { width: 100%; min-width: 0; margin-top: 7px; min-height: 46px; padding: 11px 10px; border: 1px solid var(--line); border-radius: 7px; background: var(--field); color: var(--ink); }
    .hint { margin: 8px 0 16px; font-size: 12px; }
    fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
    legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 550; }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
    button { min-height: 44px; margin-top: 14px; padding: 10px 0; border: 0; background: transparent; color: var(--accent); cursor: pointer; font-size: 13px; font-weight: 500; }
    button:disabled { opacity: .55; cursor: wait; }
    #status { min-height: 20px; margin-top: 14px; color: var(--muted); font-size: 13px; }
    #status:empty { display: none; }
    #status[data-error="true"] { color: var(--error); }
    h2 { margin: 0; font-size: 16px; font-weight: 600; }
    .note { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 12px; }
    @media (max-width: 380px) { body { padding: 18px; } .fields { grid-template-columns: minmax(0, 1fr); } button { width: 100%; } }

    main { max-width: 620px; } .fields { margin-bottom: 14px; } .wide { grid-column: 1 / -1; }
    textarea { width:100%; min-width:0; margin-top:7px; padding:11px 10px; border:1px solid var(--line); border-radius:7px; background:var(--field); color:var(--ink); font:inherit; resize:vertical; }
    .result { border-top:1px solid var(--line); margin-top:10px; padding-top:18px; }
    .answer { font-size:clamp(27px,6vw,38px); font-weight:600; color:var(--accent); letter-spacing:-.04em; }
    .detail { font-size:13px; color:var(--muted); overflow-wrap:anywhere; margin-top:8px; }
    dl { margin:12px 0 0; } dl div { display:grid; grid-template-columns: minmax(0,1fr) minmax(0,1.6fr); gap:12px; padding:11px 0; border-bottom:1px solid var(--line); }
    dt { color:var(--muted); font-size:13px; } dd { margin:0; font-size:14px; overflow-wrap:anywhere; }
    fieldset { border-top:1px solid var(--line); padding-top:12px; margin:16px 0; } legend { padding-right:10px; }
</style></head>
<body><main>
<!-- example:form -->
<form id="lookup-form" aria-busy="false">
<h2 id="business-name"></h2><p id="business-hours" class="detail"></p>
<p class="hint">Fictional shop · Closed on US observed federal holidays.</p>
<section id="result" class="result" role="status" aria-live="polite" aria-atomic="true" aria-label="Opening status"><p id="answer" class="answer">Checking hours…</p><p id="summary" class="detail"></p><p id="details" class="detail"></p></section>
<button id="submit" type="submit">Refresh hours</button><p id="status" role="status" aria-live="polite" aria-atomic="true"></p></form>
<!-- /example:form -->
</main><script>
    const API_KEY = 'YOUR_PUBLIC_API_KEY';

    const $ = selector => document.querySelector(selector);
    const form = $('#lookup-form'), button = $('#submit'), status = $('#status'), result = $('#result');
    let revision = 0, controller;
    function setStatus(message, error = false) { status.textContent = message; status.dataset.error = String(error); }
    function cancel() {
      revision++; controller?.abort(); controller = undefined;
      $('#answer').textContent = 'Checking hours…'; $('#summary').textContent = ''; $('#details').textContent = '';
      button.disabled = false; form.setAttribute('aria-busy', 'false');
    }
    async function getJson(path, params, signal) {
      if (!API_KEY.startsWith('parse_public_')) throw new Error('Add your public API key and allow this hostname.');
      const url = new URL(path, 'https://api.parseapi.com');
      url.search = new URLSearchParams({ ...params, key: API_KEY });
      const response = await fetch(url, { signal });
      if (!response.ok) {
        const message = response.status === 401 || response.status === 403 ? 'Check your public key and allowed hostnames.'
          : response.status === 429 ? 'The request limit was reached. Try again later.'
          : response.status === 404 ? 'This lookup is unavailable for the selected place or date.'
          : 'The lookup failed. Try again later.';
        throw new Error(message);
      }
      return response.json();
    }
    async function run(task) {
      cancel(); if (!form.reportValidity()) return;
      const current = revision, request = new AbortController(); controller = request;
      let timedOut = false;
      const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 12000);
      button.disabled = true; form.setAttribute('aria-busy', 'true'); setStatus('Checking...');
      try {
        const answer = await task(request.signal);
        if (current !== revision) return;
        if (request.signal.aborted) throw new Error('The request was cancelled.');
        showResult(answer); result.hidden = false;
      } catch (error) {
        if (current !== revision) return;
        request.abort();
        $('#answer').textContent = 'Hours unavailable'; result.hidden = false;
        setStatus(timedOut ? 'The request timed out. Try again.'
          : error instanceof TypeError ? 'Could not reach ParseAPI. Check your connection.'
          : error instanceof SyntaxError ? 'The response could not be read. Try again.'
          : error instanceof Error ? error.message : 'Could not complete the lookup. Try again.', true);
      } finally {
        clearTimeout(timeout);
        if (current === revision) { button.disabled = false; form.setAttribute('aria-busy', 'false'); controller = undefined; }
      }
    }

    const DAY = 86400000, MINUTE = 60000;
    function dateMs(date) {
      if (typeof date !== 'string' || !/^20\d{2}-\d{2}-\d{2}$/.test(date)) throw new Error('Choose a date between 2000 and 2099.');
      const ms = Date.parse(`${date}T00:00:00Z`);
      if (!Number.isFinite(ms) || new Date(ms).toISOString().slice(0, 10) !== date) throw new Error('Choose a valid calendar date.');
      return ms;
    }
    function clockMinutes(value) {
      if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value)) throw new Error('Use times in HH:MM format.');
      return Number(value.slice(0, 2)) * 60 + Number(value.slice(3));
    }
    function offsetMinutes(value) {
      const match = typeof value === 'string' && value.match(/^([+-])(\d{2}):([0-5]\d)$/);
      if (!match) throw new Error('The time-zone offset could not be verified.');
      const minutes = (Number(match[2]) * 60 + Number(match[3])) * (match[1] === '-' ? -1 : 1);
      if (Math.abs(minutes) > 840) throw new Error('The time-zone offset is out of range.');
      return minutes;
    }
    function offsetText(minutes) {
      return `${minutes < 0 ? '-' : '+'}${String(Math.floor(Math.abs(minutes) / 60)).padStart(2, '0')}:${String(Math.abs(minutes) % 60).padStart(2, '0')}`;
    }

    // Change this configuration to use your business's maintained schedule.
    // example:config
    const BUSINESS = {
      name: 'Sample shop', city: 'New York',
      location: { zone: 'America/New_York', country: 'US', region: null },
      opens: 9 * 60, closes: 17 * 60, week: 'weekdays', closures: new Set(), holidayPolicy: 'public'
    };
    // /example:config
    const clockLabel = minutes => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
    function displayTime(minutes) {
      if (minutes === 0) return 'midnight';
      const hour = Math.floor(minutes / 60), rest = minutes % 60;
      return `${hour % 12 || 12}${rest ? ':' + String(rest).padStart(2, '0') : ''} ${hour < 12 ? 'AM' : 'PM'}`;
    }
    $('#business-name').textContent = BUSINESS.name;
    $('#business-hours').textContent = `${BUSINESS.city} · ${BUSINESS.week === 'daily' ? 'Every day' : 'Monday–Friday'}, ${clockLabel(BUSINESS.opens)}–${clockLabel(BUSINESS.closes)} local time.`;
    function readLocal(data, zone, instant) {
      const target = data?.to;
      const match = typeof target?.at === 'string' && target.at.match(/^(\d{4}-\d{2}-\d{2})T((?:[01]\d|2[0-3]):[0-5]\d):([0-5]\d)([+-]\d{2}:[0-5]\d)$/);
      if (data?.timezone !== 'UTC' || data.at !== instant.replace('Z', '+00:00') || target?.timezone !== zone || !match
        || target.offset !== match[4] || Date.parse(target.at) !== Date.parse(instant)) throw new Error('The local time could not be verified.');
      offsetMinutes(target.offset);
      dateMs(match[1]);
      return { date: match[1], time: match[2], minutes: clockMinutes(match[2]), at: target.at, zone };
    }
    function readCalendar(data, location, year) {
      const fail = () => new Error(`The ${year} holiday calendar could not be verified.`);
      if (!data || data.country !== location.country || data.year !== year || !Array.isArray(data.holidays) || !data.holidays.length) throw fail();
      const closed = new Map();
      for (const row of data.holidays) {
        if (!row || typeof row.name !== 'string' || !row.name.trim() || !['public', 'observance'].includes(row.type)
          || !(row.regions === null || Array.isArray(row.regions) && row.regions.length > 0 && row.regions.every(region => typeof region === 'string' && /^[A-Z0-9-]+$/.test(region)))) throw fail();
        try { dateMs(row.date); } catch { throw fail(); }
        if (Number(row.date.slice(0, 4)) !== year) throw fail();
        if (row.type !== 'public' || row.regions !== null && !row.regions.includes(location.region)) continue;
        closed.set(row.date, [...(closed.get(row.date) || []), row.name]);
      }
      return closed;
    }
    // example:request
    async function checkBusiness(config, signal) {
      const { location, instant, opens, closes, week, closures } = config;
      if (!location || typeof location.zone !== 'string' || !location.zone || !/^[A-Z]{2}$/.test(location.country) ||
          (location.region !== null && typeof location.region !== 'string') ||
          ![opens, closes].every(value => Number.isInteger(value) && value >= 0 && value < 1440) || opens === closes ||
          !['daily', 'weekdays'].includes(week) || Object.prototype.toString.call(closures) !== '[object Set]' || !['public', 'none'].includes(config.holidayPolicy)) {
        throw new Error('Opening hours need a maintained schedule and explicit holiday policy.');
      }
      for (const date of closures) dateMs(date);
      const data = await getJson('/time/UTC', { to: location.zone, at: instant }, signal);
      if (signal.aborted) throw new Error('The opening-hours check was cancelled.');
      const local = readLocal(data, location.zone, instant);
      const previous = new Date(dateMs(local.date) - DAY).toISOString().slice(0, 10);
      const next = new Date(dateMs(local.date) + DAY).toISOString().slice(0, 10);
      const dates = closes < opens ? local.minutes < closes ? [previous, local.date] : [local.date, next] : [local.date];
      const years = [...new Set(dates.map(date => Number(date.slice(0, 4))))];
      let holidays;
      try {
        const calendars = config.holidayPolicy === 'none' ? [] : await Promise.all(years.map(async year => readCalendar(
          await getJson(`/holiday/${location.country}`, { year: String(year) }, signal), location, year)));
        holidays = new Map(calendars.flatMap(calendar => [...calendar]));
      } catch (error) {
        if (signal.aborted) throw error;
        return { state: 'Unknown', reason: 'The required holiday calendar is unavailable. No opening decision was made.', local, instant };
      }
      return { ...openingStatus(local, { opens, closes, week, closures }, holidays), local, instant };
    }
    // /example:request

    // example:calculate
    function openingStatus(local, config, holidays) {
      const { opens, closes, week, closures } = config;
      const previous = new Date(dateMs(local.date) - DAY).toISOString().slice(0, 10);
      const openingDay = date => week === 'daily' || ![0, 6].includes(new Date(dateMs(date)).getUTCDay());
      const closure = date => closures.has(date) ? 'Your extra closure' : holidays.has(date) ? holidays.get(date).join(' · ') : null;
      const todayClosure = closure(local.date);
      if (todayClosure) return { state: 'Closed', reason: `${local.date}: ${todayClosure}.` };
      let startDay = null;
      if (closes > opens) {
        if (openingDay(local.date) && local.minutes >= opens && local.minutes < closes) startDay = local.date;
      } else {
        if (openingDay(local.date) && local.minutes >= opens) startDay = local.date;
        else if (openingDay(previous) && local.minutes < closes) startDay = previous;
      }
      if (!startDay) return { state: 'Closed', reason: 'Outside the configured weekly opening hours.' };
      const startClosure = closure(startDay);
      if (startClosure) return { state: 'Closed', reason: `The shift's opening date is closed: ${startClosure}.` };
      const next = new Date(dateMs(local.date) + DAY).toISOString().slice(0, 10);
      const until = closes < opens && startDay === local.date && closure(next) ? 0 : closes;
      return { state: 'Open', until, reason: `Open until ${displayTime(until)} local time.` };
    }
    // /example:calculate

    // example:result
    function showResult(answer) {
      $('#answer').textContent = answer.state === 'Open' ? `Open until ${displayTime(answer.until)}` : answer.state === 'Unknown' ? 'Hours unavailable' : 'Closed';
      $('#summary').textContent = answer.state === 'Open' ? '' : answer.reason;
      $('#details').textContent = `Updated at ${displayTime(answer.local.minutes)} · ${BUSINESS.city}`;
      $('#details').setAttribute('title', answer.local.at);
      setStatus(answer.state === 'Unknown' ? 'Opening status is unknown. Try the check again later.' : '', answer.state === 'Unknown');
    }
    // /example:result
    function refreshBusiness() {
      return run(signal => {
        const { opens, closes, week, closures } = BUSINESS;
        if (![opens, closes].every(value => Number.isInteger(value) && value >= 0 && value < 1440) || opens === closes) {
          throw new Error('Set different opening and closing minutes between 0 and 1439.');
        }
        if (!['daily', 'weekdays'].includes(week)) throw new Error('Set opening days to weekdays or daily.');
        for (const date of closures) dateMs(date);
        return checkBusiness({ ...BUSINESS, instant: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z') }, signal);
      });
    }
    form.addEventListener('submit', event => { event.preventDefault(); return refreshBusiness(); });
    const initialCheck = refreshBusiness();

</script></body></html>

Need a key? Set up this browser example. For every field and parameter, see Time API reference and Holiday API reference.

Build something else

All tutorials →