---
title: "Book a held job"
method: POST
path: "/api/v1/book"
---

# Book a held job

`POST /api/v1/book`

## Request

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

Commits the hold with the customer's details. State may be booked, or pending_approval if the owner must say yes (an ask-first task or condition, or any special request): poll status until decide_by. M-24b: the owner has up to 2 hours from book to answer, never later than an hour before the job (decide_by says when, and status.hold_expires_at then equals it; the hold reply's hold_expires_at is the agent's own, short, time to book). A job starting within about an hour keeps its hold's time. Poll status about once a minute. Jobs waiting count toward your open holds. While waiting the time stays reserved; at decide_by with no answer the job is expired: the job ends and the reserved time is released. You may cancel while waiting. Booking a job already booked answers ALREADY_BOOKED; a cancelled one ALREADY_CANCELLED; any other state INVALID_STATE. M-18: a hold that ran out answers expired with alternatives (other times here, other businesses; never one that breaks a must); a job the owner declined is refused INVALID_STATE (state declined) with alternatives; a business that stopped taking jobs since the hold answers BUSINESS_UNAVAILABLE, the job is called off and the time let go, with alternatives at other businesses.

## Request body

- object
  - `transaction_id` string, uuid, required
  - `customer` object, required
    - `name` string, required
    - `phone` string, required
    - `email` string
    - `service_address` string, required
    - `access_instructions` string: How the cleaner gets in: a gate or door code, where a key or lockbox is. Optional. Kept with the customer's details and shown to the owner only after they say yes, like the address. Never returned to you, in book, status or anywhere else.
  - `idempotency_key` string, required: Unique per write request (a UUID). Retrying with the same key returns the original result.

## Response `200`

Success

- object
  - `transaction_id` string, uuid, required
  - `state` 'booked' | 'pending_approval' | 'declined' | 'expired', 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.
  - `receipt` object, nullable: What was booked, from the record: a business editing its prices, limits or name later never changes it. text is a short plain confirmation to show your customer (the business's own clock, no ids). null for a job with no time yet.
    - `transaction_id` string, uuid
    - `business` object
      - `slug` string
      - `display_name` string
    - `service` object
      - `key` string
      - `name` string
    - `window` object, nullable
      - `start` string, date-time
      - `end` string, date-time
      - `time_zone` string: The business's time zone (IANA).
    - `price` object, nullable
      - `amount` number
      - `currency` string
    - `home` string, nullable
    - `scope` object: Labels, never keys.
      - `included` string[]
      - `extras` string[]
      - `not_doing` string[]
      - `conditions` string[]
      - `approved_requests` string[]
    - `cancel` object
      - `allowed` boolean
      - `fee` unknown
      - `how` string
    - `change` object
      - `allowed` boolean
      - `how` string
    - `state` string
    - `text` string
  - `decide_by` string, date-time
  - `reason` string
  - `next_available` object, nullable: The first same-business time in alternatives, kept for agents that read it.
  - `alternatives` object[]: Ready alternatives for the same job, worked out on every answer by the same judge as search: up to 3 other times at this business and up to 3 other businesses. An alternative never misses a must-have, never offers a must-not and never adds back a task the customer left out; a nice-to-have it misses is named in why. Each is priced for its own window (its day and hour charges): hold its quote_token with its allocation_id as it is.
    - `kind` 'same_business_other_time' | 'other_business_same_job', required
    - `slug` string, required
    - `display_name` string, required
    - `service_key` string, required
    - `allocation_id` string, uuid, required
    - `window_start` string, date-time, required
    - `window_end` string, date-time, required
    - `job_hours` integer, nullable: How long this job takes on site, in whole hours: hourly services the hours asked (rounded up); other services the booking length plus the business's bigger-homes hours when the home meets its rule. For a business that builds its open times from its working hours (discover capacity.jobs_at_once), every offered time is exactly this long.
    - `job_people` integer, nullable: How many cleaners this job takes: an hourly price's people, the business's usual crew (crew.min), more when the home meets its crew rule (crew.rule), at least 2 for a must-not crew.solo or must-have crew.two_or_more, 3 for crew.three_or_more. Counted against capacity.cleaners. null while the quote is incomplete.
    - `price` object, required
      - `amount` number
      - `currency` string
    - `quote_token` string, required
    - `expires_at` string, date-time, required
    - `booking_authority` object, nullable
    - `why` string[], required: Fixed codes: other_time, other_business, nice_to_have_missed:<word>, needs_owner_approval.
  - `alternatives_note` 'timed_out' | 'none_found': Why alternatives is empty: none fit every must, or they could not be worked out within 4 seconds (the refusal is never delayed for them).
  - `if_changed` object[]: Businesses that could do this job if the customer gave up ONE thing (up to 2). None of them can be held or booked from this item: it carries no quote_token and no allocation_id. Ask the customer; if they agree, search again without change.word (that is a new job). A must-have the business said it does not offer, or a must-not that is its only option, is the only kind of change listed; never something the business has not answered, what it takes (the home), a condition of the home, or a second change. You can still quote such a business by name, but do not book it with the job unchanged without the customer's yes.
    - `slug` string, required
    - `display_name` string, required
    - `service_key` string, required
    - `change` object, required: The one thing the customer would give up: a word out of must_have or out of must_not, with the business's own reason.
      - `from` 'must_have' | 'must_not'
      - `word` string
      - `code` string
      - `label` string
    - `price_if_changed` object, required: The changed job's search price at this business: the same number a search without change.word shows. Not the price at earliest_window_start when the business charges for days or hours.
      - `amount` number
      - `currency` string
    - `earliest_window_start` string, date-time, nullable, required
    - `nice_to_have_missed` string[], required
    - `needs_owner_approval` boolean, required: The changed job would wait for the owner's yes there.
    - `next_step` 'ask_customer_then_search', required
  - `if_changed_note` 'timed_out' | 'none_found': Why if_changed is empty: no business is one change away, or they could not be worked out within the 4 seconds (the ready alternatives are kept).
  - `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.

## 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)
