# quote.dev — Quote & Deposit API for AI Agents

Estimates are non-binding; deposit holds require explicit authorization. quote.dev is never Merchant of Record: providers settle through Stripe Connect Direct Charges on their own account (Stripe-managed negative-balance liability) or their own checkout. Public providers are fixtures, not verified studios. Keep keys server-side, out of URLs/logs.

Sandbox qd_test_... creates estimates and idempotent POST /v1/quotes with pi_sandbox_... manual-capture holds at $0 charged; verify Ed25519 dev.uqp.quoting.quote_lock via /.well-known/jwks.json. Live holds fail closed unless LIVE_CONNECT_ENABLED=true, Stripe secret and provider account are configured.

## Register once (REST or CLI, not MCP)

POST /v1/auth/agent-register with owner_email and agent_name. No sandbox human gate.

```sh
curl -fsS https://api.quote.dev/v1/auth/agent-register -H 'Content-Type: application/json' -d '{"owner_email":"owner@example.com","agent_name":"my-agent"}'
quotedev register --email owner@example.com --name my-agent --json
```

HTTP 201 returns api_key (qd_test_...), key_prefix, mode "test", scopes, claim_url, claim_code, poll_token, expires_at. Store the one-time key securely; only its hash is stored. On 429 wait Retry-After seconds.

## Discover intake and fixtures (public)

GET https://api.quote.dev/v1/packs returns pack schemas and sandbox counts. GET /v1/providers supports pack, tier A/B/C, active and sandbox filters. Public reads have short cache lifetimes.

Canonical packs: tattoo/custom (alias tattoo), medspa/laser-removal (alias laser-removal). Use pack schemas for factors, enums and questions. Keep image bytes client-side. Unsure goals, PMU and multiple tattoos require consultation. Live tattoo fixture id: sandbox-tattoo-a.

## Estimate ($0, no customer contact data)

```sh
curl -fsS https://api.quote.dev/v1/estimates -H 'Content-Type: application/json' -H 'Authorization: Bearer qd_test_REPLACE_ME' -d '{"pack_id":"tattoo/custom","factors":{"description":"Geometric mountain","size":"sm","placement":["Upper Arm"],"complexity":"simple","style":"blackgrey"}}'
quotedev packs --json
quotedev providers --pack tattoo/custom --json
quotedev estimate --pack tattoo/custom --factors '{"description":"Geometric mountain","size":"sm","placement":["Upper Arm"],"complexity":"simple","style":"blackgrey"}' --json
```

dry_run:true remains accepted for compatibility. Never send customer phone/email, including nested fields; these are rejected before forwarding. Responses: aggregate/candidate low/mid/high (min/max aliases), USD minor units (100 = USD 1), C62 sessions, deposit_preview (capture_method:"manual", charged:false), matched_providers_count, calibration_score (0–1 accuracy), p50_approval_seconds, void_rate, dispute_rate, assumptions, mode:"sandbox". Estimates have $0 fees.

HTTP 422: error:"intake_incomplete", missing_fields[] (field, type, description, enum/minimum/maximum), suggested_user_questions[]. Ask once, then retry. Estimates are non-binding.

## Exactly four MCP tools

Connect Streamable HTTP at https://mcp.quote.dev/mcp:
- quote_discover_packs: packs, schemas, fixture counts and telemetry.
- quote_estimate: /v1/estimates, including actionable 422 passthrough.
- quote_hold_deposit: POST /v1/quotes; pass idempotency_key and the quote body fields below.
- quote_status: GET /v1/quotes/:quote_id; OAuth requires quote_id; direct API keys may omit it for private claim status. Poll tokens are REST-only. All private tools require bearer auth.

Registration: REST/CLI only. SDK: @quotedev/sdk. CLI: @quotedev/cli (init/register, packs, providers, estimate, quote hold/status/authorize/cancel/accept-counter; --json and --help).

## Create and authorize a quote

POST /v1/quotes with Bearer qd_test_... and mandatory Idempotency-Key. JSON: {"provider_ids":["sandbox-tattoo-a"],"pack_id":"tattoo/custom","locked_total_minor":68000,"deposit_minor":15000,"currency":"USD"}. Integer minor units; choose 1–2 providers (more returns 422, never truncates). Keys and quotes are isolated by sandbox/live mode. Same key/body replays; different body returns 409. Reuse the key on retries.

GET /v1/quotes/:id reads persisted state. POST /v1/quotes/:id/authorize authorizes the hold (optional payment_method); POST /v1/quotes/:id/accept-counter accepts revised terms; POST /v1/quotes/:id/cancel cancels. States: VALIDATED_PENDING_REVIEW, APPROVED, COUNTERED, DECLINED, EXPIRED. Default TTL is 7 days.

qd_test_... uses deterministic pi_sandbox_... holds: no Stripe HTTP and $0 charged/fees. A live request without payment_method returns requires_escalation:true and continue_url https://pay.quote.dev/q/{quote_id}. Holds are manual-capture Stripe Connect Direct Charges on the provider account; $0 platform fee on declined, expired or uncaptured holds.

Hosted /q/:quote_id shows deposit, countdown and "$0 if declined or expired"; test authorization is simulated. /connect configures provider pricing/brand and connections. /dashboard has Keys & Claim, Live Quotes, Deposit Holds, Webhooks & Logs, Calibration & Pricebook.

Provider connect: POST /v1/providers/connect with a private provider_id and pack_ids (default tattoo/custom), then POST /v1/quotes. Public fixtures return 403 fixture_read_only; test fixture settlement using settlement_rail on /v1/quotes. Live connect requires LIVE_CONNECT_ENABLED=true. Signed action token required for POST /v1/providers/quotes/:id/decision (approve/counter/decline). Ed25519 dev.uqp.quoting.quote_lock version "2026-11-05"; public keys at https://api.quote.dev/.well-known/jwks.json. The signed quote_hash binds the returned locked_quote snapshot. Sandbox signatures use a public test fixture key, never live trust. Verify agent webhook X-QuoteDev-Signature (HMAC-SHA256) over the raw payload before trusting decisions.

- settlement_rail: stripe_connect_direct | byo_provider_checkout | none; merchant_of_record: provider. BYO has 0% router fee; none never captures.
- POST /v1/providers/connect uses a private provider_id; accepts settlement_mode: byo_link and HTTPS byo_checkout_url; /authorize returns escalation.continue_url for BYO.
- POST /v1/quotes/:id/outcome records final_invoice_subtotal and outcome_status; live requires APPROVED. Fixture calibration_preview is agent-scoped and simulated; public rankings stay fixed. GET /v1/providers/:id/calibration returns mode-partitioned scores and sample_size.

## Optional asynchronous human claim

Send claim_url privately to the consenting owner. An operator must provision a verified human account; public signup does not grant verified status. Owner signs in at POST /v1/auth/human/sign-in/email, retains the cookie, then confirms POST /v1/auth/claim/:code from the API origin. Claim requires emailVerified:true and a session email matching the registered owner_email; GET has no side effects. Never give an agent the owner's password.

Poll GET /v1/auth/agent-status with Bearer poll_token or test key. Pending means continue sandbox work. After claim, qd_live_... is retrievable once; claim alone does not configure payments. Live holds require LIVE_CONNECT_ENABLED=true plus Stripe/account configuration. POST /v1/auth/revoke with the current bearer revokes all agent credentials. Claims expire at expires_at.

## References and boundaries

- OpenAPI: https://api.quote.dev/openapi.json; reference: /reference.
- Discovery: https://quote.dev/.well-known/ucp (UCP 2026-08-25).
- UQP Layer 1: https://uqp.dev/docs (Apache-2.0), external to commercial Layer 2.
- OAuth 2.1 (MCP): /.well-known/oauth-protected-resource; /oauth/register (exact redirect URIs); /oauth/authorize + /oauth/token with PKCE S256 + state; no refresh tokens. Resource https://mcp.quote.dev/mcp (local http://localhost:8788/mcp). Better Auth human sessions approve an owned claimed agent_id and mode; live quotes:write is operator-provisioned. A 401 sends WWW-Authenticate.
