Skip to main content

Get business config

Returns the stored ReadyConfig for the specified business. This is the same configuration injected into the business’s Ready MCP server at deploy time as EZFORGE_READY_CONFIG. Responses are cached client-side with Cache-Control: private, max-age=10. Config changes propagate to clients within 10 seconds.

Authentication

This endpoint accepts two auth modes: Session callers are scoped to their own businesses. API-key callers are scoped to businesses owned by the user associated with the key’s project. In both cases, if the business does not belong to the authenticated caller, the endpoint returns 404 (not 403) to avoid leaking existence.

Path parameters


Response 200

ReadyConfig schema

This is the v3.0 shape (READY_CONFIG_VERSION = '3.0'). The prior v2.0 shape (flat hours map, required catalog.items[].categoryId, no menus) is no longer served by this endpoint; it is kept internally as ReadyConfigV2 only for code that still needs to read an already-deployed v2.0 payload during the rollout.

catalog block

The platform populates catalog from the business’s full item library, including each item’s aliases and availabilityWindows:
  • Full library. catalog.items always lists every catalog item, including items that also appear on a menu. Menu sections[].items[] reference these by itemId, so an item is never dropped from catalog because it is on a menu.
  • aliases. An array of alternate names, sorted by code-unit order (case-sensitive and locale-independent, so uppercase sorts before lowercase and accented characters sort after z). Omitted (not []) when the item has none.
  • availabilityWindows. An array of windows, omitted (not []) when the item has none; absence means always available, subject to isAvailable. Each window may carry dayOfWeek, start, end (time range), startDate, endDate (date range) and recurringAnnually; fields that are not set on a window are omitted, and recurringAnnually appears only when true. Windows are sorted deterministically (by dayOfWeek, start, end, startDate, endDate, then recurringAnnually, with an unset value sorting first), so regenerating an unchanged catalog yields an identical catalog payload.
  • priceType. One of fixed, starting_at, hourly, free_estimate or range. priceCents is omitted for quote-only items that have no price.
  • range items. A range item is a price spread (e.g. $50–$150). priceCents is the lower bound, and the upper bound is carried in metadata alongside it. When a range item has both a lower and an upper bound, metadata includes price_range_min_cents (equal to priceCents) and price_range_max_cents (the upper bound), both in cents. These two keys always reflect the item’s current stored bounds: they override any same-named keys in the item’s stored metadata, which can be stale after the item’s price is edited. A range item with no upper bound set yet, or no lower bound, omits both keys and carries its stored metadata unchanged. Non-range items never get these keys added.
  • Absent when empty. catalog is omitted when the business has no categories and no items.
The platform populates menus from the business’s menu configuration. Menu items are emitted compactly to keep the payload small: each item carries itemId and sortOrder plus only the override fields that differ from its catalog.items[] entry, and consumers resolve every omitted field from catalog.items[] by itemId:
  • Per-field override resolution. Each menu item’s name, description, priceCents and priceType resolve independently, consumer-side: the field on the menu item if present (an override), otherwise the value from the item’s catalog.items[] entry (override ?? library). Overriding only the price, for example, emits only priceCents and still inherits the catalog name, description and price type. An override equal to the catalog value is omitted. itemId always points at the catalog.items[] entry. A resolved description or priceCents is absent when neither the override nor the catalog item has a value. Legacy payloads with fully inline menu items resolve identically.
  • Active menus only. Inactive menus are never emitted. When the business has no active menus, menus is omitted entirely rather than sent as []. catalog.items[] is unaffected either way (see the catalog block above), and featureFlags.menuScheduling is still derived from the emitted menus.
  • Ordering. Menus, and the sections within each menu, are ordered by sortOrder ascending, with the entity’s id as a tiebreak so equal sortOrder values still produce a stable order. Items within a section are ordered the same way. Regenerating unchanged data yields an identical menus payload.
  • schedule windows. Each window has an optional dayOfWeek plus start and end as "HH:MM" strings (seconds from the database are dropped). A window with no dayOfWeek applies on every day. A missing bound is emitted as "00:00": a missing start means from the start of the day, and a missing end means until end of day (an end of "00:00" is treated as end-of-day, not midnight at the start, consistent with the interval-set evaluator below). So a day-only window covers that whole day, and a window with no day or times covers all day on every day. A menu that has such a window is still scheduled, so it counts toward menuScheduling, unlike a menu with an empty schedule, which is always on. Windows are sorted by dayOfWeek (all-days first), then start, end, and id.
  • Dangling items are skipped. A menu item whose catalog item is not in the business’s catalog.items[] is left out, so every itemId in menus resolves to a catalog.items[] entry.

hours block

Precedence. When a special entry keyed "YYYY-MM-DD" and one keyed "MM-DD" (recurringAnnually: true) both match the same calendar date, the YYYY-MM-DD entry takes precedence and the MM-DD entry is ignored for that date. For example, given both a "2026-12-25" entry and a "12-25" (recurringAnnually: true) entry in hours.special: on 2026-12-25, the "2026-12-25" entry applies; on 2027-12-25 (no exact-date entry that year), the "12-25" entry applies. Business-local “now” resolution (in progress). A resolveLocalNow helper exists to derive the business’s current local day of week, time, and calendar date from timezone using the IANA tz database via Intl.DateTimeFormat, never a fixed UTC offset, so the result stays correct across both the spring-forward and fall-back DST transitions — and to raise an error for an unresolvable or invalid timezone rather than silently falling back to UTC, since a wrong-but-plausible local time could misapply a business’s hours. When “now”-based hours evaluation (matching regular/special/closures against the present moment) is wired up to use it, this is how that evaluation will work. Today, the platform’s one “open now” evaluator still derives day-of-week and time from UTC rather than from timezone. Full precedence chain (in progress). A resolveEffectiveHours helper exists to resolve the complete closures > special > regular precedence chain for a given business-local date: a matching closure (exact-range or annual-recurring month/day range, wrapping across the year boundary, e.g. Dec 24 → Jan 2) always means closed regardless of special/regular; otherwise a matching special entry (per the “YYYY-MM-DD” vs “MM-DD” rule above) replaces regular; otherwise the day’s regular interval set applies. This helper is pure (no DB reads) and takes localDate/localDay as input — it is designed to consume resolveLocalNow’s output but does not call it directly. Like resolveLocalNow, it is not yet wired into the platform’s “open now” evaluator. Interval-set evaluation, roles, and last-order (in progress). A second resolveOpenNow (distinct from, and not yet used by, the DB-backed one in hub/search.ts) composes resolveLocalNow and resolveEffectiveHours to evaluate “is this business open now” against a full interval set per day rather than a single open/close pair — supporting split hours (e.g. separate lunch and dinner intervals) and an optional role scope (service, kitchen, bar; defaults to service, which falls back to the full interval set when no service-tagged interval exists, but an explicit non-service role never falls back). It checks today’s interval set for a same-day match — an overnight interval (open > close, except a close of "00:00", which means end-of-day, not overnight) belongs to the day it starts on, so it matches only from its opening time (Wednesday 18:00–02:00 is not open Wednesday 01:00 unless Tuesday carries a tail) — OR’d with yesterday’s set carrying its overnight tail into today’s early morning, with yesterday’s set resolved through the same closure/special/regular precedence chain so a closure or special-hours entry on yesterday’s date correctly suppresses or alters the carry-forward. A companion resolveLastOrder computes close − offset (wrapping across midnight) unless the interval already carries a precomputed lastOrder, and resolveCurrentOrNextLastOrder exposes that value for the current-or-next interval in a role, scoped to today only (an overnight carry-forward’s last-order was already relevant before midnight). Like resolveEffectiveHours, this module is pure and not yet wired into the platform’s “open now” evaluator. Item availability evaluation (in progress). A resolveItemAvailableNow helper evaluates whether a catalog.items[] entry’s availabilityWindows (see the catalog row above) permit it to be ordered right now: the item is available iff isAvailable is true AND (it has no windows, or at least one window matches). isAvailable: false is an immediate manual 86 that overrides every window. Each window matches when all of its set constraints hold — dayOfWeek, an overnight-aware [start, end) time range (reusing the same isTimeInRange semantics as the interval-set evaluator above), and an exact- or annual-recurring-date range (reusing the same range-matching helpers as resolveEffectiveHours) — and an overnight window (start later than end) belongs to the day it starts on: its late-night head is checked against that day’s dayOfWeek and date range, and its early-morning tail against the previous day’s, so a Friday 22:00–02:00 window matches Saturday 01:00 but not Friday 01:00. Multiple windows on an item are OR’d together, since AvailabilityWindow has no overlap-precedence rule of its own. This helper is pure and not yet wired into the platform’s catalog-serving path. Active-menu resolution and price-now precedence (in progress). A resolveMenuActiveNow helper resolves whether a single menu is active at a given local time: a menu with no schedule rows (e.g. the default “Main” menu) is always-on, otherwise it is active when at least one MenuSchedule row matches on both day-of-week and time range (reusing the same overnight-aware time-range matcher that resolveOpenNow uses above (isTimeInRange and isOvernightRange in intervals.ts), with a day-scoped overnight row adding its own head/tail split on top). A day-scoped overnight row (start later than end) belongs to the day it starts on, so a Friday 22:00–02:00 row is active Friday night and Saturday 01:00 but not Friday 01:00. resolveActiveMenus applies this to a full menu list and returns all currently-active menus — overlaps are allowed and expected (e.g. a Lunch menu and a Happy Hour menu can both be active at 14:30) — ordered by ascending sortOrder. A gap (no scheduled menu currently active) is not a special case: an always-on default menu is already present in the result whenever no scheduled menu matches, so it naturally fills the gap. resolvePriceNow answers “what does this catalog item cost right now” by scanning resolveActiveMenus’s output in that same ascending-sortOrder order and returning the first section item matching the requested item ID — so when the same item appears on more than one active menu, the lowest-sortOrder active menu wins the single “price now” answer. Like the other evaluators on this page, this module is pure and not yet wired into a platform endpoint.

Assembly from stored business data

The platform builds the config’s business data from a business’s stored records through a single assembler, assembleBusinessProfile(db, businessId), so every config generation reads the same sources rather than each caller building its own copy. This is an internal platform function, not an HTTP endpoint; this section describes how its output surfaces in the ReadyConfig above:
  • Sources. Business identity (name, description, website, category) comes from the business record, contact and location fields (phone, address, city, state, zip, country, coordinates) and timezone from the business profile, hours from the regular, special and closure hours records, catalog and menus from the catalog and menu records, and featureFlags from the business’s capabilities. The assembler omits any contact or identity field that is not set; ConfigGenerator then fills string fields (business.description, contact.phone, contact.email, and contact.address.line1/city/state/zip/country) with "" and defaults business.category to the config’s templateType, so the served config always carries those keys.
  • Configured hours are always carried through. A business with configured hours produces a populated hours block. The assembler does not emit an empty hours object for a business that has hours set.
  • Recurring special hours. A special-hours entry marked as recurring is keyed by the year-independent "MM-DD" form, not by the literal year of the stored date, so an annual closure (e.g. Dec 25) matches in every year. One-off entries keep the full "YYYY-MM-DD" key.
  • Roles in special hours. A special-hours entry replaces the whole day for every role (service, kitchen, bar), so its intervals are exactly its open rows; a closed row of any role contributes no interval. To close only the kitchen on a holiday, list the service interval(s) and omit the kitchen interval. An entry whose rows are all closed closes the whole day.
  • Last-order bounds. An interval’s lastOrder is close minus the configured last-order offset, wrapping across midnight. If the offset is longer than the interval (close minus the offset would land before open), lastOrder is omitted rather than reported as an incoherent cutoff. An offset exactly equal to the interval’s length yields lastOrder equal to open, which is kept. An overnight interval (close earlier than open) measures its length across midnight. Write-time validation only checks that the offset is within 0–1440 minutes, so this assembly step is where the bound is enforced.
  • Kitchen and bar hours stay in hours. Only service intervals count as the business’s canonical operating hours used for the single open/close pair a day (the first open service row wins, and a day with no open service row is closed). kitchen and bar intervals appear only in the interval-based hours block above, and are not reported as the venue’s opening hours by the hours endpoint (GET /api/v1/ready/businesses/{id}/hours) or indexed as service hours in Hub search.
  • Capability flags. Capabilities map to feature flags as appointment_booking → appointmentBooking, online_ordering → onlineOrdering, quote_requests → quoteRequests, faq → faq, menu_browsing → menuBrowsing. A capability with any other name is ignored. A flag with no corresponding capability record is emitted as false. menuScheduling is not a capability; it is derived and set to true only when at least one emitted menu has a schedule.
  • serverId and templateType are not assembled. serverId identifies the server a config is deployed to, so it is attached at deploy time rather than read from business data. templateType is not derived from the business’s free-text industry (that value is surfaced only as business.category); when ConfigGenerator is called without it (e.g. previews), the config uses professional_services. A deploy regeneration instead keeps the business’s already-assigned type and fails with TEMPLATE_TYPE_UNKNOWN if none exists and no override is supplied.
  • Missing profile. A business whose profile has not been saved yet still assembles: contact fields are empty strings and hours.timezone is an empty string, so the generated v3.0 config fails validation until the profile (including its timezone) is saved. A business that does not exist is a not-found error rather than an empty config.

Error codes