---
title: "Business days calculator: add or count working days with your weekend mask and holiday list"
method: GET
path: "/v1/time/business-days"
tags: ["deterministic", "no-api-key", "time", "calendar", "business-days"]
---

# Business days calculator: add or count working days with your weekend mask and holiday list

`GET /v1/time/business-days`

## Request

- Base URL: `https://api.duoleads.com`
- URL: `https://api.duoleads.com/v1/time/business-days`
- Auth: none declared

Business days calculator with a caller-supplied calendar: add or subtract N working days from a date, or count the working days between two dates. The weekend is a weekday mask (Friday-Saturday and Sunday-only weeks work) and holidays are an explicit date list, so nothing depends on a stale holiday table. Returns the result date, the day count and the skipped dates with reasons. The skipped list is capped at 1,000 entries (skipped_truncated flags it); skipped_total is always exact.

**Output (`data`):** add: result (YYYY-MM-DD), from, days, calendar_days_spanned, skipped [{date, reason weekend|holiday, weekday}] (first 1,000), skipped_total, skipped_truncated. count: business_days, calendar_days, weekend_days, holiday_days, holidays_matched, direction (forward|backward) (holidays on a weekend are reported as weekend, never as holiday). count is half-open [from, to) unless inclusive says otherwise; when to precedes from the same interval is counted backwards and business_days is still a non-negative count. skipped is in the order the dates were traversed and lists at most 1,000 entries; skipped_total is the full count and skipped_truncated is true when the list was cut (the cap only binds for days beyond roughly 1,400 working-day skips, e.g. days near 3,650 with a two-day weekend); result, calendar_days_spanned and skipped_total are always exact.

Deterministic: identical input returns identical output, so the response is served with `Cache-Control: private, max-age=31536000, immutable` (the paying client may cache it indefinitely; shared caches must not, because the response is paid).

Price: $0.05 USDC per call (x402, Base). Route contract version 1.0.0. Docs: https://api.duoleads.com/docs#time-business-days

## Query parameters

- `op` 'add' | 'count'
- `from` string, date, required
- `days` integer
- `to` string, date
- `weekend` string
- `holidays` string
- `inclusive` 'start' | 'both' | 'neither'

## Response `200`

Paid and settled. `data` is route-specific; a route that judges its input returns 200 with `data.valid: false` for a bad value.

- object
  - `ok` unknown, required
  - `route` '/v1/time/business-days', required
  - `version` '1.0.0', required
  - `data` object, required: add: result (YYYY-MM-DD), from, days, calendar_days_spanned, skipped [{date, reason weekend|holiday, weekday}] (first 1,000), skipped_total, skipped_truncated. count: business_days, calendar_days, weekend_days, holiday_days, holidays_matched, direction (forward|backward) (holidays on a weekend are reported as weekend, never as holiday). count is half-open [from, to) unless inclusive says otherwise; when to precedes from the same interval is counted backwards and business_days is still a non-negative count. skipped is in the order the dates were traversed and lists at most 1,000 entries; skipped_total is the full count and skipped_truncated is true when the list was cut (the cap only binds for days beyond roughly 1,400 working-day skips, e.g. days near 3,650 with a two-day weekend); result, calendar_days_spanned and skipped_total are always exact.
    - `result` string
    - `from` string
    - `days` integer
    - `calendar_days_spanned` integer
    - `skipped` object[]
      - `date` string, required
      - `reason` string, required
      - `weekday` string, required
    - `skipped_total` integer
    - `skipped_truncated` boolean
    - `business_days` integer
    - `calendar_days` integer
    - `weekend_days` integer
    - `holiday_days` integer
    - `holidays_matched` string[]
    - `direction` string
  - `meta` Meta, required
    - `disclaimer` string, required: Legal short disclaimer, identical on every response.
    - `request_id` string, required: ULID.
    - `computed_at` string, date-time, required: RFC 3339 UTC with milliseconds.
    - `price_usd` string, required: Decimal string, "0.05".
    - `deterministic` boolean, required
    - `sources` object[]: Present only on routes that read a vendored dataset: the exact edition that answered.
      - `id` string, required
      - `version` string, required
      - `published` string, required
      - `attribution` string
      - `licence` string

## Other responses

- `400`: MISSING_PARAM, INVALID_PARAM or UNSUPPORTED_VALUE. Not charged: validation runs before the paywall.
- `402`: Payment required. No valid PAYMENT-SIGNATURE was supplied: read the base64 PAYMENT-REQUIRED header (or this body) for the price ($0.05 = 50000 atomic USDC on eip155:8453), sign an EIP-3009 authorization and retry. Also returned when settlement is declined (PAYMENT_FAILED). Nothing is charged on a 402.
- `404`: NOT_FOUND: unknown path. Not charged.
- `405`: METHOD_NOT_ALLOWED: only GET and HEAD are served. Not charged.
- `413`: INPUT_TOO_LARGE: a parameter exceeded its documented maximum length. Not charged.
- `500`: INTERNAL: a server bug. Not charged.

---

[API](https://skmtc.dev/duoleads/apis/duo-data-utilities-iban-isin-vat-tax-id-barcode-timezone-geo.md) · [All operations](https://skmtc.dev/duoleads/apis/duo-data-utilities-iban-isin-vat-tax-id-barcode-timezone-geo/llms.txt) · [OpenAPI document](https://skmtc.dev/duoleads/apis/duo-data-utilities-iban-isin-vat-tax-id-barcode-timezone-geo/revisions/18790d59615e?raw)
