Agent API

CommonTape lets an AI agent discover, quote, hold, book, modify, cancel and check the status of a home-services job on a customer’s behalf, against the rules each business sets. It is free to use, and payment-agnostic: the customer pays the business directly. CommonTape is pre-launch, so real merchants are limited for now.

Connect

Try it with the demo key

You can try the whole path, from search to booked, without writing to us. The demo key reaches only CommonTape Demo Cleaning (commontape-demo-cleaning), a pretend business in ZIP 10001. It never reaches a real business, and nobody is sent anything.

curl -X POST https://commontape.com/api/v1/search \
  -H "Authorization: Bearer ct_demo_nQz4WaXqJvMoL8A6FYCaRtpJnsF7UrZh" \
  -H "Content-Type: application/json" \
  -d '{"zip":"10001","inputs":{"bedrooms":2,"bathrooms":1}}'
curl -X POST https://commontape.com/api/v1/hold \
  -H "Authorization: Bearer ct_demo_nQz4WaXqJvMoL8A6FYCaRtpJnsF7UrZh" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"quote_token":"<a match's quote.quote_token>",
       "allocation_id":"<one of its available_windows[].allocation_id>"}'

Operations

What each tool does to data

Read tools change nothing. Write tools record or hold something that runs out or can be replaced. Sensitive writes commit the customer or cannot be undone: confirm the details with the customer every time before calling one.

A job is one record, in one of ten states

From the moment you call quote, the job is one record with a transaction_id that never changes. The job itself is kept as numbered versions: every change (quoting it again with its transaction_id) is a new version beside the old one, never written over. Everything the customer asked for (must-haves, nice-to-haves, must-nots, rooms left out, special requests) is kept with each version and carried to every later step. A search result is not a promise: prices and windows are as of availability_checked_at, and only a booked job is committed. Every quote and booking is stamped with the business’s versions (passport, eligibility, pricing, booking authority, word list).

Every answer says the job’s state, its allowed actions (allowed_actions) and what to do next (next_actions):

Anything not allowed in a state is refused, never guessed: a cancelled job cannot be booked, a held job cannot be held again, a booked job is changed only through a new quote and modify.

Books on its own, or waits for the owner

Every match and every ready quote carries booking_authority: auto (the business lets it book on its own) or approval_required with its reasons, decided by one rule from the owner’s booking setting, the job and the price. 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, special_request, or the ask-first key itself (for example cond.very_dirty). Read it, never the prose. book may still ask when search said auto (the owner tightened the setting since); it never books on its own when search said the owner must approve, never the other way round.

booking_authority is the business’s say-so only. Your customer’s permission to book or to spend that amount is not something CommonTape knows: you are responsible for your own customer’s say-so before you call book. Once booked, status shows commitment: supplier is rule (booked under the owner’s setting) or merchant_action (the owner said yes), and customer is agent_assertion (your call), never a customer confirmation CommonTape did not see.

Waiting for the owner

A job needs the owner’s yes when booking_authority says so. book then answers pending_approval and status shows approval.needs (what needs approval, each with why), approval.asked_at and the approval.deadline. The time stays reserved while waiting (approval.capacity_held). With no answer by the deadline the job is expired: the job ends and the reserved time is released. You may cancel while waiting; approval.reasons says why it was asked. A no is a business answer, not a failure: it comes back as declined with the owner’s decline.reason, its canonical decline.code (for example schedule_conflict, outside_service_area, insufficient_staff, property_too_large, condition_not_accepted, unable_to_meet_special_request, unsupported_request; the last four only when the job has a stated home size, a stated condition, a special request or an asked-for task), decline.next_actions and decline.next_available.

A booked job becomes completed when the owner marks it done, or 24 hours after its window ends; completion.source says which (merchant or system). An owner calling off a booked job is supplier_cancelled, apart from your cancelled, with the owner’s reason in cancellation. Status then carries recovery: the booked job (as changed, constraints unchanged; send it to search again) and other businesses for it. A change of that job answers SUPPLIER_CANCELLED with the same recovery.

Who sees what: Before the owner says yes they see the customer's first name and initial, the ZIP, the job and the time. After a yes (booked or done) they also see the full name, phone, address and access instructions. A job that never reached the owner, or that ended without a yes, shows nothing of the customer. Agents never get the customer's details back.

Trust: what is known, who vouches for it, until when

Every business in a search answer (matches, needs_more, owner_must_quote and near_misses) carries trust. Each fact in trust.facts says who vouches for it in provenance: commontape_checked (CommonTape checked the evidence itself), or owner_stated (the owner says so; CommonTape has not checked it, and the words are the owner’s own). The kinds are switched_on (CommonTape reviewed the business and switched it on), insurance, licence (the number’s last 4 only) and outside_rating (a rating elsewhere, as the owner typed it until CommonTape checks it). Each fact has as_of and expires_on, its last current day in the business’s time zone. Insurance and a licence are no longer current the day after they are valid through; an outside rating 90 days after its as_of day; switch-on never expires. A fact that has lapsed moves to trust.expired with expired_on: never shown as current, never dropped without saying so. A newer fact of a kind replaces the older one.

trust.history (commontape_observed) is the business’s job record on CommonTape, all time, counted from its jobs on every answer, never typed by anyone: jobs done and who marked them, the owner’s yes and no, jobs called off by the owner, jobs that ran out of time waiting for the owner, and the owner’s median minutes to answer. Each rate comes with the count it is out of; a new business answers zeros. Nothing marks a job done by an agent yet, so that count is 0 for now.

Trust never changes the order of results, the price, who is eligible or what needs the owner’s approval. It is facts for you to weigh: no score, badge or rank.

Examples

curl -X POST https://commontape.com/api/v1/quote \
  -H "Authorization: Bearer $COMMONTAPE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"merchant_slug":"acme-cleaning","service_key":"house_cleaning.standard",
       "inputs":{"bedrooms":2,"bathrooms":1,"customer_zip":"10001"}}'
curl -X POST https://commontape.com/api/v1/book \
  -H "Authorization: Bearer $COMMONTAPE_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id":"<from hold>",
       "customer":{"name":"Jane Doe","phone":"5551234567","service_address":"1 Main St, New York, NY"}}'

Idempotency

Write operations (hold, book, modify, cancel) require an idempotency key, and quote takes an optional one (8–128 characters, a UUID works): header Idempotency-Key or body field idempotency_key. Retrying with the same key returns the original result; reusing a key with a different request returns IDEMPOTENCY_KEY_CONFLICT.

Quotes, holds and approvals

A quote_token expires shortly after it is issued. A hold locks the price: its 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. Holds release their slot when they expire. Some merchants approve each booking; book then returns pending_approval and the slot stays held until decide_by.

If the business changed anything since the quote, the hold judges the same job again. Same price: the hold goes ahead. A lower price: it is held and price_changed says so. A higher price: PRICE_CHANGED, with price_changed (old amount, new amount, currency and the price lines that changed) and a fresh quote_token for the same job at the new price; hold that, or send your own ceiling for the call in accept_price_up_to and a new price at or under it is held and reported. A business that stopped taking jobs, or no longer does this job (a must-have it no longer offers), answers BUSINESS_UNAVAILABLE with its reasons, at hold or at book.

One open hold per job per business: holding the same job again at the same business answers HOLD_EXISTS with your open hold’s transaction_id. If you serve several customers, send customer_ref (your own label, never shown to the owner) on search or quote so two customers with the same job are told apart. You may hold the same job at different businesses; cancel the ones you do not book.

When a slot is gone, the business stopped taking jobs, the price moved, a hold ran out or the owner said no, the answer carries alternatives: up to three other times at this business and up to three other businesses, each with its price, window, a quote_token to hold as it is, and why (other_time, other_business, nice_to_have_missed:<word>, needs_owner_approval). An alternative never breaks a must: it never misses a must-have, never offers a must-not and never adds back a task the customer left out. An empty list comes with alternatives_note.

Beside it, if_changed lists up to two businesses that could do the job if the customer gave up one thing: change names it (a must_have word the business said it does not offer, or a must_not word that is its only option) with the business’s reason, and price_if_changed is that changed job’s search price (not the price at earliest_window_start when the business charges for days or hours). An if_changed business cannot be held as it stands: the item carries no quote_token and no allocation_id. Ask the customer; if they agree, search again without that word, which is a new job. You can still quote that business by name, but do not book it with the job unchanged without the customer’s yes. An empty list comes with if_changed_note.

status carries alternatives and if_changed too, on a dead end reached in the last 24 hours: declined (other times here, not the declined one, and other businesses), expired (other times here and, for a job that came from a search, other businesses), cancelled because the business stopped taking jobs, and supplier_cancelled (other businesses only). Status keeps them up to 60 seconds: polls in the same minute read the same list and tokens, and alternatives_worked_out_at says when they were worked out.

Search places full matches before conditional ones (never outranked on price), then most nice-to-haves met, then lowest price; order_keys says so as keys, and each match carries ranking: its position, earliest time, nice-to-haves met of those wanted, whether the owner must approve, and that the ZIP is in its area. CommonTape has no distance; travel says when the ZIP is further out.

Open times fit the job. A business that has said how many jobs it can do at the same time (discover capacity.jobs_at_once) has its open times built from its working hours for the next 14 days: each working day split into bookings of the job’s length, from its start time. Every quote, search match and alternative says that length as job_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, which may need bedrooms or square_feet first). Each offered time is exactly job_hours long, and is offered only while the business has fewer jobs at the same time than it can do; slots_remaining is how many more it can take then. A hold of a time shorter than the job (or longer) answers OUTSIDE_HOURS, and so does a time that is no longer in the business’s hours; quote again for current times. A job longer than the longest job the business takes, or than its working day, is a near miss job_too_long. A change that makes a booked job longer needs a new time of the new length.

Cleaners and the time between jobs. Every quote, search match and alternative also says job_people, how many cleaners the job takes: an hourly price’s people, the business’s usual crew (crew.min), more when the home meets its crew rule, and at least 2 when the job asks for no solo cleaner (null while the quote is incomplete). The crew rule crew.rule may read bedrooms, bathrooms, living spaces or floors, or square feet (by is one of bedrooms, bathrooms, living_areas, floors, square_feet, from from); a quote that needs that fact and does not state it is incomplete, and floors and living spaces are read from property only. A business may say how many cleaners it has in all (capacity.cleaners): jobs at the same time never need more, so a time is offered only when this job’s cleaners fit, and a hold that would pass it answers CAPACITY_FULL. It may also say the time it needs between jobs (capacity.gap_minutes): built times step by the job’s length plus that gap, and two jobs closer together than it count as at the same time. A change that keeps its time but needs more cleaners than are free there answers CAPACITY_FULL: pick a new time.

Errors

Errors are {"error":{"code","message","next_actions","state"}} (MCP: a tool error with the same body). The code is authoritative; the message is for a person; next_actions says what to try; state is the job’s state when the refusal is about it. Job outcomes use ten fixed codes, transaction codes say the job is not in a state that allows the call, and protocol codes cover the request itself:

QUOTE_EXPIRED: the quote ran out or a newer one replaced it. INVALID_STATE: not allowed in the job’s state (read status). ALREADY_BOOKED, ALREADY_CANCELLED: said plainly. MODIFICATION_CONFLICT: the job changed while you acted, or the quote is not this job’s. HOLD_EXPIRED means only a hold that ran out. Protocol codes: UNAUTHORIZED (401), RATE_LIMITED (429), INVALID_REQUEST (400), NOT_FOUND (404), IDEMPOTENCY_KEY_CONFLICT (409), TEMPORARY_ERROR (500, safe to retry).

Limits

Each key has a per-minute request limit and a cap on open holds. Holds you let run out unused count too: at five in 24 hours (per key) the next hold answers RATE_LIMITED with retry_after_seconds. A hold that ran out while the owner was deciding does not count, nor does a decline or a cancel. A retry of a hold that already succeeded is always answered. A job belonging to another key is always reported as NOT_FOUND.

Data

You send the customer’s contact details only at book. They go to the merchant and are deleted if the job doesn’t happen. See our Privacy Policy. Public merchant records are at /m/<slug> and /m/<slug>/record.json; machine-readable index: /llms.txt.