---
title: "Hold a window"
method: POST
path: "/api/v1/hold"
---

# Hold a window

`POST /api/v1/hold`

## Request

- Base URL: `https://commontape.com`
- URL: `https://commontape.com/api/v1/hold`
- Auth: HTTP bearer

Moves the quoted job to held: reserves one slot from the quote (quoted -> held, the same transaction_id). A quote that ran out answers QUOTE_EXPIRED; a quote replaced by a newer one for the same job answers QUOTE_EXPIRED too. Reserves one slot from the quote. Holds expire; book before hold_expires_at. The quote must state exactly the day and hour conditions listed on the window (available_windows[].conditions); otherwise PRICE_MISMATCH says which condition to add or drop. The rooms the job covers (scope) come from the quote token: hold takes no scope of its own and echoes the quote's. M-18: a hold locks the price: the answer says what it holds (quote_id, price, window, booking_authority), and a change the owner makes after the hold applies to new quotes only. If the business changed since the quote, the same job is judged again: at the same price the hold goes ahead; at a lower price it is held and price_changed says so; at a higher price it is refused PRICE_CHANGED (old and new amount, the reasons, and a fresh quote_token for the new price) unless you sent accept_price_up_to at or above it. A business that stopped taking jobs, or no longer does this job, answers BUSINESS_UNAVAILABLE with its reasons. M-21: a time that is too soon (the business's notice, the ZIP's notice, or its same-day / next-day No in discover when) answers INSUFFICIENT_NOTICE (422, the same next_actions as OUTSIDE_HOURS: select_alternative_window, quote) with error.earliest (RFC 3339, business offset): no start before it is taken, though a later time may still be closed; a weekend, evening or holiday No answers OUTSIDE_HOURS. One open hold per job per business: holding the same job again at the same business answers HOLD_EXISTS with the open job's transaction_id (send a different customer_ref for another customer). Holds you let run out count against you: at max_abandoned_holds_per_day (5 by default) in 24 hours the next hold answers RATE_LIMITED with retry_after_seconds. A refusal you can recover from (SLOT_TAKEN, CAPACITY_FULL, OUTSIDE_HOURS, BUSINESS_UNAVAILABLE, PRICE_CHANGED) carries alternatives. A time busy in the business's own calendar feed (read again right here) answers CALENDAR_BUSY (409) with the next free window in alternatives.

## Request body

- object
  - `quote_token` string, required
  - `allocation_id` string, uuid, required
  - `idempotency_key` string, required: Unique per write request (a UUID). Retrying with the same key returns the original result.
  - `accept_price_up_to` number: Optional. Your own ceiling for this call (your customer's say-so, not CommonTape's): if the price moved up since the quote and is at or under this, it is held and price_changed says so. A lower price is always held and reported.

## Response `200`

Success

- object
  - `transaction_id` string, uuid, required
  - `state` 'held', required: One of ten states, each with one meaning. quote_incomplete: Not priced yet: the job needs more facts. needs says which. Nothing is held. Allowed: quote. quoted: An exact price for one exact job version, until expires_at. Nothing is held: the time is not reserved yet. Allowed: hold, quote. held: A time is reserved for this job until hold_expires_at. Book it, or cancel to let it go. Allowed: book, cancel. pending_approval: Asked to book; the owner must say yes by approval.deadline. The time stays reserved while waiting. No answer by then and the job expires and the time is released. Allowed: status, cancel. booked: The owner is committed to do this job at this price and time. Allowed: status, quote, modify, cancel. completed: The job was done. completion.source says who marked it: the owner (merchant) or CommonTape after the window ended (system). Allowed: status. declined: The owner said no. decline.reason says why; decline.next_available is the next open time when there is one. Allowed: status, quote. cancelled: Called off by the agent. The time was released. Allowed: status, quote. supplier_cancelled: Called off by the owner after it was booked. The job can be quoted again elsewhere from job. Allowed: status, quote. expired: Ran out of time: the quote passed expires_at, or the hold or the owner's answer passed its deadline. The time was released. Allowed: status, quote.
  - `allowed_actions` string[]: The operations you may call on this job now. Anything else is refused (INVALID_STATE, ALREADY_BOOKED, ALREADY_CANCELLED).
  - `next_actions` string[]: The suggested next steps, from a fixed list.
  - `job_version` integer, nullable: The job's version. Every change to the job is a new version; old versions, their quotes and prices are kept, never written over.
  - `hold_expires_at` string, date-time, required
  - `quote_id` string, uuid, required: The quote held: a new one when the price moved.
  - `price` object, required: The price this hold locks.
    - `amount` number, required
    - `currency` string, required
  - `window` object, required
    - `allocation_id` string, uuid, required
    - `window_start` string, date-time, required
  - `booking_authority` object, required: Whether the business lets this job book on its own (auto) or the owner must say yes first (approval_required), decided by one rule from the owner's booking setting, the job and the price; never read it from prose. reasons: owner_approves_every_job (the owner asks every time), over_auto_book_limit (the price is above the owner's book-for-me limit), business_not_listed (not open to agents yet), special_request (a free-text request), or the ask-first key itself (e.g. cond.very_dirty, task.oven_interior). book may still ask when this said auto (the owner tightened the setting since), never the reverse. This is the business's say-so only: your customer's permission to book or spend is yours to keep.
    - `mode` 'auto' | 'approval_required', required
    - `reasons` string[], required
    - `version` integer, required: The owner's booking setting's version (versions.booking_authority).
  - `price_changed` object: The price moved between the quote and the hold.
    - `old_amount` number, required
    - `new_amount` number, required
    - `currency` string, required
    - `reasons` string[], required: The price lines that were added, removed or changed (breakdown keys such as base, bedrooms, cond.pets, minimum_adjustment), or price_rules_changed when the business changed how it prices.
  - `taxonomy_version` integer, required: The word list version this was made under (GET /api/v1/taxonomy/house_cleaning returns the current one). A job made before reads as 4.
  - `scope` object: The rooms the job covers: leave_out (rooms not to clean) or clean_only (the only rooms to clean), never both, by space key from property.spaces in the word list. A must-have task in a room left out is refused. Under a by_rooms price a room left out comes off the price (its line is gone); a fixed, by_home, square-foot or hourly price does not change. It is echoed on every match, needs_more, near miss, quote and hold, and shown to the owner.
    - `leave_out` string[]
    - `clean_only` string[]

## Other responses

- `default`: Error

---

[API](https://skmtc.dev/commontape/apis/commontape-agent-api.md) · [All operations](https://skmtc.dev/commontape/apis/commontape-agent-api/llms.txt) · [OpenAPI document](https://skmtc.dev/commontape/apis/commontape-agent-api/revisions/714820e6cc4d?raw)
