Show whether a business is open now in JavaScript

Show whether a configured business is open now using its local hours and holiday calendar.

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

Working example

HTML + JavaScript

Start with an opening-status display

Press Check now to see whether a sample New York shop is open. Its hours are Monday–Friday, 09:00–17:00, and it closes on observed US federal holidays. The result includes the reason and time checked.

Create a Public key on the keys page, allow localhost and your site's hostname, and replace YOUR_PUBLIC_API_KEY in the downloaded HTML. Keep secret keys on your server. Serve the file's folder:

Terminal
python3 -m http.server 8000

Open the local example.

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()
};

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

Check now captures one UTC instant from the browser clock and converts it with /timezone/UTC?to={timezone}&at={time}. Use the returned local date to select the holiday year.

JavaScript
async function checkBusiness(config, signal) {
  const { location, instant, opens, closes, week, closures } = config;
  const data = await getJson('/timezone/UTC', { to: location.zone, at: instant }, signal);
  const local = readLocal(data, location.zone, instant);
  const previous = new Date(dateMs(local.date) - DAY).toISOString().slice(0, 10);
  const dates = closes < opens && local.minutes < closes ? [previous, local.date] : [local.date];
  const years = [...new Set(dates.map(date => Number(date.slice(0, 4))))];
  let holidays;
  try {
    const calendars = 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 };
}

An overnight shift at New Year can need both years' calendars. The complete source checks the Timezone 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 Unknown.

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}.` };
  return { state: 'Open', reason: `Within the shift that starts on ${startDay}.` };
}

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.

Each result is a snapshot. Check now refreshes it; no background requests run. Timeouts and errors stay visible, and old responses cannot replace a newer check. The checked local timestamp includes its UTC offset.

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: 46px; margin-top: 16px; padding: 11px 18px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
    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">Closed on US observed federal holidays.</p>
<button id="submit" type="submit">Check now</button><p id="status" role="status" aria-live="polite" aria-atomic="true"></p></form>
<!-- /example:form -->
<section id="result" class="result" role="status" aria-live="polite" aria-atomic="true" hidden aria-label="Opening status"><p id="answer" class="answer"></p><p id="summary" class="detail"></p><p id="details" class="detail"></p></section></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; result.hidden = true;
      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();
        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()
    };
    // /example:config
    const clockLabel = minutes => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
    $('#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
        || !Number.isInteger(target.offset_minutes) || offsetMinutes(target.offset) !== target.offset_minutes
        || target.offset !== match[4] || Date.parse(target.at) !== Date.parse(instant)) throw new Error('The local time could not be verified.');
      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;
      const data = await getJson('/timezone/UTC', { to: location.zone, at: instant }, signal);
      const local = readLocal(data, location.zone, instant);
      const previous = new Date(dateMs(local.date) - DAY).toISOString().slice(0, 10);
      const dates = closes < opens && local.minutes < closes ? [previous, local.date] : [local.date];
      const years = [...new Set(dates.map(date => Number(date.slice(0, 4))))];
      let holidays;
      try {
        const calendars = 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}.` };
      return { state: 'Open', reason: `Within the shift that starts on ${startDay}.` };
    }
    // /example:calculate

    // example:result
    function showResult(answer) {
      $('#answer').textContent = answer.state;
      $('#summary').textContent = answer.reason;
      $('#details').textContent = `Local clock: ${answer.local.at.replace('T', ' ')} · ${answer.local.zone}. Checked for ${answer.instant}.`;
      setStatus(answer.state === 'Unknown' ? 'Opening status is unknown. Try the check again later.' : '', answer.state === 'Unknown');
    }
    // /example:result
    form.addEventListener('submit', event => {
      event.preventDefault();
      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);
      });
    });

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

Need a key? Create a public API key. For every field and parameter, see Timezone API reference and Holiday API reference.

Build something else

All tutorials →