On this page
Specification · 0.1-draft
One contract, from questions to quote.
UQP gives businesses and agents a shared JSON vocabulary for services without a fixed shelf price. The core defines the objects; industry packs define the questions and pricing math. This draft is not stable.
Start here
- Read the objects: discover questions with a manifest, collect answers, then evaluate the business model.
- Try the sample JSON: follow the synthetic Tattoo pack examples below. Validate them against the raw schemas.
- Pick a transport: REST and MCP have local reference bindings. They are not production services.
For a local reference implementation, clone the repository, install dependencies and run:
npm ci
npm test
npm run typecheckThe pure Tattoo evaluator accepts answers plus explicit consent, a pricing model and a caller-supplied timestamp. See Tattoo pack implementation notes before using its unvalidated reference settings.
Core objects
All schemas use JSON Schema draft 2020-12. Object roots reject unknown properties. Tables below are generated from the checked-in schemas, including nested fields and shared definitions.
These examples use Tattoo pack 0.1.0 and a fixed evaluation time. The model constants are unvalidated; the seven-day expiry is an illustrative business choice, not a protocol default. Example data, not real prices.
QuoteManifest
A QuoteManifest declares the intake questions, model reference, review mode and validity settings offered by a business.
| Field | Type | Required | Description |
|---|---|---|---|
pack_id | string | Yes | Industry pack identifier. Minimum length: 1. |
pack_version | string | Yes | Selected pack version; must agree across related objects. Minimum length: 1. |
enabled_questions | array of string | Yes | Question IDs enabled by the business model. Items must be unique. Item constraints: Minimum length: 1. |
pricing_model_ref | string | Yes | Reference to the business-owned model; consumers resolve it. Minimum length: 1. |
artist_review_mode | enum | Yes | Business review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price. |
expiry_days | integer | Yes | Business-configured validity period in days; no protocol default. Minimum: 1. |
currencies | array of string | Yes | Supported three-letter uppercase currency codes. Minimum items: 1. Items must be unique. Item constraints: Pattern: ^[A-Z]{3}$. |
{
"pack_id": "tattoo",
"pack_version": "0.1.0",
"enabled_questions": [
"description",
"placement",
"size",
"width",
"height",
"complexity",
"style",
"budget_min",
"budget_max",
"first_tattoo"
],
"pricing_model_ref": "urn:uqp:tattoo:pricing:example",
"artist_review_mode": "post_price",
"expiry_days": 7,
"currencies": [
"USD"
]
}QuoteRequest
A QuoteRequest carries raw answers and explicit consent for a selected industry pack and version.
| Field | Type | Required | Description |
|---|---|---|---|
pack_id | string | Yes | Industry pack identifier. Minimum length: 1. |
pack_version | string | Yes | Selected pack version; must agree across related objects. Minimum length: 1. |
answers | object | Yes | Exact raw question answers. Keep contact details in a separate channel; free text still needs privacy checks. Known contact-information keys are rejected. Map values may be any JSON value. |
consent | object | Yes | Explicit research-sharing and anonymization preferences, with capture time. Unknown properties are rejected. |
consent.share_for_pricing_research | boolean | Yes | Whether sharing for pricing research was authorized. |
consent.anonymize | boolean | Yes | Requested anonymization preference, not proof that the artifact is anonymous. |
consent.captured_at | string | Yes | When consent was recorded. Format: date-time. |
{
"pack_id": "tattoo",
"pack_version": "0.1.0",
"answers": {
"description": "A small geometric mountain design with clean outlines.",
"size": "sm",
"placement": [
"Lower Arm"
],
"complexity": "simple",
"style": "blackgrey"
},
"consent": {
"share_for_pricing_research": false,
"anonymize": true,
"captured_at": "2026-11-05T12:00:00Z"
}
}PricingModel
A PricingModel records the business-owned rates, factor switches, rounding and deposit rules used to calculate a non-binding Estimate.
| Field | Type | Required | Description |
|---|---|---|---|
pack_id | string | Yes | Industry pack identifier. Minimum length: 1. |
pack_version | string | Yes | Selected pack version; must agree across related objects. Minimum length: 1. |
id | string | No | Identifier for this model or factor. Minimum length: 1. |
artist_review_mode | enum | No | Business review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price. |
factors | array of object | Yes | Business-configurable pricing switches and parameters. Item constraints: Unknown properties are rejected. |
factors[].id | string | Yes | Identifier for this model or factor. Minimum length: 1. |
factors[].enabled | boolean | Yes | Whether this factor participates in pricing. |
factors[].weight | number | No | Optional factor weight, interpreted by the pack. |
factors[].value | any JSON | No | Pack-defined value; any JSON value is permitted. |
factors[].mode | string | No | Pack-defined calculation mode, except the fixed rounding modes. Minimum length: 1. |
factors[].provenance | string | No | Source of settings or price, including who stated the price and when. Minimum length: 1. |
rates | object | Yes | Pack-defined rate map, including currency. Map values may be any JSON value. |
minimums | object | Yes | Pack-defined business minimums. Map values may be any JSON value. |
rounding | object | Yes | Increment and direction used to round prices. Unknown properties are rejected. |
rounding.increment | number | Yes | Positive rounding increment. Greater than 0. |
rounding.mode | enum | Yes | Pack-defined calculation mode, except the fixed rounding modes. Allowed: nearest, up, down. |
deposit_policy | object | Yes | Pack-defined deposit calculation policy. Unknown properties are rejected. |
deposit_policy.mode | string | Yes | Pack-defined calculation mode, except the fixed rounding modes. Minimum length: 1. |
deposit_policy.amount | number | No | Nonnegative deposit amount. Minimum: 0. |
deposit_policy.percentage | number | No | Fraction from zero to one, not a whole-number percent. Minimum: 0. Maximum: 1. |
deposit_policy.basis | string | No | Pricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Minimum length: 1. |
deposit_policy.rounding_increment | number or null | No | Pack-defined field. Greater than 0. |
deposit_policy.policies | array of string | No | Deposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1. |
expiry_days | integer | Yes | Business-configured validity period in days; no protocol default. Minimum: 1. |
provenance | string | No | Source of settings or price, including who stated the price and when. Minimum length: 1. |
keyword_match | enum | No | Pack-defined field. Allowed: substring, whole_word. |
{
"pack_id": "tattoo",
"pack_version": "0.1.0",
"provenance": "tattoo.co current constant, unvalidated",
"id": "tattoo-formula-a-example",
"artist_review_mode": "post_price",
"factors": [
{
"id": "size",
"enabled": true,
"provenance": "tattoo.co current constant, unvalidated",
"value": {
"dimensions": {
"xs": 1.5,
"sm": 3,
"md": 5,
"lg": 8,
"xl": 12,
"unsure": 5
},
"area_per_hour": 12,
"minimum_hours": 1,
"unknown_base_hours": 4
},
"mode": "no_estimate"
},
{
"id": "placement",
"enabled": true,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "additive_0_5h",
"value": {
"additive_hours": 0.5,
"multiplier": 1.2,
"tricky": [
"Wrist",
"Hand",
"Neck",
"Foot",
"Lower Leg"
],
"match": {
"additive_0_5h": "substring",
"multiplier_1_2": "substring_ci"
}
}
},
{
"id": "complexity",
"enabled": true,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "asked",
"value": {
"simple": 1,
"moderate": 1.15,
"detailed": 1.35,
"realism": 1.6
}
},
{
"id": "style",
"enabled": true,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "asked",
"value": {
"blackgrey": 1,
"popcolor": 1.15,
"color": 1.3
}
},
{
"id": "wraparound",
"enabled": false,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "wraparound_multiplier_1_3",
"weight": 1.3
},
{
"id": "tier",
"enabled": true,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "fallback_standard"
},
{
"id": "deposit",
"enabled": true,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "pct_of_low_30",
"value": {
"low_percentage": 0.3,
"total_percentage": 0.5,
"low_increment": 25,
"total_increment": 50,
"piece_increment": 25
}
},
{
"id": "sessions",
"enabled": false,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "not_yet_modeled"
},
{
"id": "cover_up",
"enabled": false,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "not_yet_modeled"
},
{
"id": "multi_area",
"enabled": false,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "not_yet_modeled"
},
{
"id": "city_rate",
"enabled": false,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "not_yet_modeled"
},
{
"id": "non_usd",
"enabled": false,
"provenance": "tattoo.co current constant, unvalidated",
"mode": "not_yet_modeled"
}
],
"rates": {
"currency": "USD",
"pricing_mode": "hourly",
"tiers": {
"apprentice": 120,
"standard": 200,
"senior": 300
},
"range_band": {
"low": 0.85,
"high": 1.15
},
"piece_high_multiplier": 1.5,
"provenance": "tattoo.co current constant, unvalidated"
},
"minimums": {
"piece_deposit": 100,
"provenance": "tattoo.co current constant, unvalidated"
},
"rounding": {
"increment": 50,
"mode": "nearest"
},
"deposit_policy": {
"mode": "pct_of_low_30",
"percentage": 0.3
},
"expiry_days": 7,
"keyword_match": "substring"
}Estimate
An Estimate is a non-binding price range computed from PricingModel and returned on PAYLOAD_SUBMITTED, even when artist review is required. It is UQP’s pre-quote layer, not an offer the client can accept.
| Field | Type | Required | Description |
|---|---|---|---|
pack_id | string | Yes | Industry pack identifier. Minimum length: 1. |
pack_version | string | Yes | Selected pack version; must agree across related objects. Minimum length: 1. |
answers | object | Yes | Exact raw question answers. Keep contact details in a separate channel; free text still needs privacy checks. Known contact-information keys are rejected. Map values may be any JSON value. |
artist_review_mode | enum | Yes | Business review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price. |
price_range | object | Yes | Required on every Estimate and Quote, including pending review. Unknown properties are rejected. |
price_range.min | number | Yes | Nonnegative lower bound; application must check min ≤ max. Minimum: 0. |
price_range.max | number | Yes | Nonnegative upper bound. Minimum: 0. |
price_range.currency | string | Yes | Three-letter uppercase currency code. Pattern: ^[A-Z]{3}$. |
price_range.basis | enum | Yes | Pricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Allowed: hourly, day_rate, from, fixed. |
price_range.method | enum | Yes | Price method: artist_stated, rule or logged_quotes; logged_quotes requires n ≥ 5. Allowed: artist_stated, rule, logged_quotes. |
price_range.n | integer | No | Number of logged quotes supporting the price; at least five for logged_quotes. Minimum: 5. |
price_range.provenance | object | Yes | Source of settings or price, including who stated the price and when. Unknown properties are rejected. |
price_range.provenance.who | string | Yes | Who stated the price. Minimum length: 1. |
price_range.provenance.when | string | Yes | When the price was stated. Format: date-time. |
sessions | object | Yes | Session estimate; provisional in the tattoo reference evaluator. Unknown properties are rejected. |
sessions.count | integer | Yes | Nonnegative session count. Minimum: 0. |
sessions.hours_per_session | number | Yes | Nonnegative estimated hours per session. Minimum: 0. |
deposit | object | Yes | Pack-defined field. Provider is merchant of record; UQP never holds funds. Only processor confirmation marks a deposit paid. Unknown properties are rejected. |
deposit.amount | number | Yes | Nonnegative deposit amount. Minimum: 0. |
deposit.basis | string | Yes | Pricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Minimum length: 1. |
deposit.policies | array of string | Yes | Deposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1. |
deposit.status | enum | Yes | Deposit remains unpaid until processor confirmation. Allowed: unpaid, paid. |
deposit.merchant_of_record | any JSON | Yes | The provider, never UQP. |
deposit.confirmation | object | No | Payment processor confirmation; client self-report is insufficient. Unknown properties are rejected. |
deposit.confirmation.source | any JSON | Yes | Payment confirmation must originate from the payment processor. |
deposit.confirmation.reference | string | Yes | Processor confirmation reference. Minimum length: 1. |
deposit.confirmation.confirmed_at | string | Yes | When the payment processor confirmed payment. Format: date-time. |
deposit.provenance | object | Yes | Source of settings or price, including who stated the price and when. Unknown properties are rejected. |
deposit.provenance.who | string | Yes | Who stated the price. Minimum length: 1. |
deposit.provenance.when | string | Yes | When the price was stated. Format: date-time. |
assumptions | array of string | Yes | Assumptions and limitations that accompany the estimate. |
intermediates | array of object | Yes | Ordered audit trail of every evaluation step, including disabled factors. Minimum items: 1. Item constraints: Unknown properties are rejected. |
intermediates[].step | string | Yes | Evaluator step name. Minimum length: 1. |
intermediates[].factor_id | string | Yes | Factor responsible for this step. Minimum length: 1. |
intermediates[].input | any JSON | Yes | Original input for the calculation; any JSON value. |
intermediates[].value | any JSON | Yes | Pack-defined value; any JSON value is permitted. |
intermediates[].skipped | any JSON | No | Pack-defined field. |
intermediates[].reason | string | No | Pack-defined field. |
review_status | enum | Yes | Current lifecycle state; allowed transitions must be enforced by the consumer. Allowed: VALIDATED_PENDING_REVIEW, MUTATION_REQUIRED. |
consent | object | Yes | Explicit research-sharing and anonymization preferences, with capture time. Unknown properties are rejected. |
consent.share_for_pricing_research | boolean | Yes | Whether sharing for pricing research was authorized. |
consent.anonymize | boolean | Yes | Requested anonymization preference, not proof that the artifact is anonymous. |
consent.captured_at | string | Yes | When consent was recorded. Format: date-time. |
anonymized | boolean | Yes | Whether the delivered artifact actually is anonymized, independently of the requested preference. |
kind | any JSON | Yes | Discriminator: estimate or quote. |
binding | any JSON | Yes | False for Estimate; true only for an artist-reviewed Quote. |
confidence | enum | Yes | Confidence in this estimate or reviewed offer; not a guarantee. Allowed: low, medium, high. |
{
"kind": "estimate",
"binding": false,
"confidence": "low",
"pack_id": "tattoo",
"pack_version": "0.1.0",
"answers": {
"description": "A small geometric mountain design with clean outlines.",
"size": "sm",
"placement": [
"Lower Arm"
],
"complexity": "simple",
"style": "blackgrey"
},
"artist_review_mode": "post_price",
"price_range": {
"min": 150,
"max": 250,
"currency": "USD",
"basis": "hourly",
"method": "rule",
"provenance": {
"who": "PricingModel tattoo@0.1.0 (example rules, unvalidated)",
"when": "2026-11-05T12:00:00.000Z"
}
},
"sessions": {
"count": 1,
"hours_per_session": 1
},
"deposit": {
"amount": 50,
"basis": "low",
"provenance": {
"who": "PricingModel tattoo@0.1.0 (example rules, unvalidated)",
"when": "2026-11-05T12:00:00.000Z"
},
"policies": [
"Example data, not real prices; provider must state final deposit policies before artist review."
],
"status": "unpaid",
"merchant_of_record": "provider"
},
"assumptions": [
"tattoo.co current constant, unvalidated; supplied handoff attributed to f018b86, not independently revalidated.",
"Pricing uses unrounded hours; display logs both Formula A Math.round(t*10)/10 and Formula B toFixed(1).",
"Sessions are a provisional single block (zero for by_piece); session splitting and caps are not modeled.",
"Multiple placements trigger any-tricky once; area summing is not modeled.",
"Raw answers are preserved; anonymized=false because free text has not been deidentified."
],
"intermediates": [
{
"step": "pricing_mode",
"factor_id": "pricing",
"input": "hourly",
"value": "hourly"
},
{
"step": "input_resolved",
"factor_id": "complexity",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": "simple"
},
{
"step": "input_resolved",
"factor_id": "style",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": "blackgrey"
},
{
"step": "input_resolved",
"factor_id": "size",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": "sm"
},
{
"step": "input_resolved",
"factor_id": "placement",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": [
"Lower Arm"
]
},
{
"step": "input_resolved",
"factor_id": "tier",
"input": {
"source": "configured",
"rule_id": null,
"keywords": []
},
"value": "fallback_standard"
},
{
"step": "input_resolved",
"factor_id": "deposit",
"input": {
"source": "configured",
"rule_id": null,
"keywords": []
},
"value": "pct_of_low_30"
},
{
"step": "dimensions",
"factor_id": "size",
"input": {
"raw_size": "sm",
"width": null,
"height": null
},
"value": {
"width": 3,
"height": 3
}
},
{
"step": "area",
"factor_id": "size",
"input": {
"width": 3,
"height": 3
},
"value": 9
},
{
"step": "base_hours",
"factor_id": "size",
"input": {
"area": 9,
"area_per_hour": 12,
"minimum_hours": 1
},
"value": 1
},
{
"step": "placement",
"factor_id": "placement",
"input": {
"hours": 1,
"placements": [
"Lower Arm"
],
"mode": "additive_0_5h",
"match": "substring",
"tricky": false,
"adjustment": 0
},
"value": 1
},
{
"step": "complexity",
"factor_id": "complexity",
"input": {
"source": "asked",
"selected": "simple",
"hours": 1
},
"value": 1
},
{
"step": "complexity_hours",
"factor_id": "complexity",
"input": {
"multiplier": 1
},
"value": 1
},
{
"step": "skipped",
"factor_id": "wraparound",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 1,
"skipped": true,
"reason": "disabled"
},
{
"step": "hours_raw",
"factor_id": "hours",
"input": {
"pricing_mode": "hourly"
},
"value": 1
},
{
"step": "hours_estimate",
"factor_id": "hours",
"input": {
"raw": 1,
"decimal_places": 1,
"formula_a": 1,
"formula_b": "1.0"
},
"value": 1
},
{
"step": "tier",
"factor_id": "tier",
"input": {
"source": "fallback_standard",
"complexity": "simple",
"style": "blackgrey",
"rule_id": null,
"keywords": [],
"used": true
},
"value": "standard"
},
{
"step": "rate",
"factor_id": "rate",
"input": {
"pricing_mode": "hourly"
},
"value": {
"amount": 200,
"source": "tier:standard"
}
},
{
"step": "style",
"factor_id": "style",
"input": {
"source": "asked",
"selected": "blackgrey"
},
"value": 1
},
{
"step": "subtotal",
"factor_id": "pricing",
"input": {
"hours": 1,
"rate": 200,
"style_multiplier": 1
},
"value": 200
},
{
"step": "shop_minimum",
"factor_id": "minimum",
"input": {
"subtotal": 200,
"minimum": 200,
"applied": false
},
"value": 200
},
{
"step": "total",
"factor_id": "pricing",
"input": {
"by_piece": false
},
"value": 200
},
{
"step": "range_band",
"factor_id": "range",
"input": {
"total": 200,
"low_multiplier": 0.85,
"high_multiplier": 1.15
},
"value": {
"low": 170,
"high": 229.99999999999997
}
},
{
"step": "range",
"factor_id": "range",
"input": {
"low": 170,
"high": 229.99999999999997,
"rounding": {
"increment": 50,
"mode": "nearest"
}
},
"value": {
"low": 150,
"high": 250
}
},
{
"step": "deposit_base",
"factor_id": "deposit",
"input": {
"basis": "low",
"base": 150,
"percentage": 0.3
},
"value": 45
},
{
"step": "deposit_raw",
"factor_id": "deposit",
"input": {
"basis": "low"
},
"value": 45
},
{
"step": "deposit",
"factor_id": "deposit",
"input": {
"mode": "pct_of_low_30",
"unrounded": 45,
"increment": 25,
"minimum": 0
},
"value": 50
},
{
"step": "skipped",
"factor_id": "sessions",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "cover_up",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "multi_area",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "city_rate",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "non_usd",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "sessions",
"factor_id": "sessions",
"input": {
"hours": 1,
"provisional": true
},
"value": {
"count": 1,
"hours_per_session": 1
}
},
{
"step": "expiry",
"factor_id": "expiry",
"input": {
"now": "2026-11-05T12:00:00.000Z",
"expiry_days": 7
},
"value": "2026-11-12T12:00:00.000Z"
}
],
"review_status": "VALIDATED_PENDING_REVIEW",
"consent": {
"share_for_pricing_research": false,
"anonymize": true,
"captured_at": "2026-11-05T12:00:00Z"
},
"anonymized": false
}Quote
A Quote is a binding offer issued only after artist review at QUOTED. It records the price range, sessions, deposit amount and policies, valid_until, assumptions, confidence, price provenance and calculation trail.
| Field | Type | Required | Description |
|---|---|---|---|
pack_id | string | Yes | Industry pack identifier. Minimum length: 1. |
pack_version | string | Yes | Selected pack version; must agree across related objects. Minimum length: 1. |
answers | object | Yes | Exact raw question answers. Keep contact details in a separate channel; free text still needs privacy checks. Known contact-information keys are rejected. Map values may be any JSON value. |
artist_review_mode | enum | Yes | Business review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price. |
price_range | object | Yes | Required on every Estimate and Quote, including pending review. Unknown properties are rejected. |
price_range.min | number | Yes | Nonnegative lower bound; application must check min ≤ max. Minimum: 0. |
price_range.max | number | Yes | Nonnegative upper bound. Minimum: 0. |
price_range.currency | string | Yes | Three-letter uppercase currency code. Pattern: ^[A-Z]{3}$. |
price_range.basis | enum | Yes | Pricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Allowed: hourly, day_rate, from, fixed. |
price_range.method | enum | Yes | Price method: artist_stated, rule or logged_quotes; logged_quotes requires n ≥ 5. Allowed: artist_stated, rule, logged_quotes. |
price_range.n | integer | No | Number of logged quotes supporting the price; at least five for logged_quotes. Minimum: 5. |
price_range.provenance | object | Yes | Source of settings or price, including who stated the price and when. Unknown properties are rejected. |
price_range.provenance.who | string | Yes | Who stated the price. Minimum length: 1. |
price_range.provenance.when | string | Yes | When the price was stated. Format: date-time. |
sessions | object | Yes | Session estimate; provisional in the tattoo reference evaluator. Unknown properties are rejected. |
sessions.count | integer | Yes | Nonnegative session count. Minimum: 0. |
sessions.hours_per_session | number | Yes | Nonnegative estimated hours per session. Minimum: 0. |
deposit | object | Yes | Pack-defined field. Provider is merchant of record; UQP never holds funds. Only processor confirmation marks a deposit paid. Unknown properties are rejected. |
deposit.amount | number | Yes | Nonnegative deposit amount. Minimum: 0. |
deposit.basis | string | Yes | Pricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Minimum length: 1. |
deposit.policies | array of string | Yes | Deposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1. |
deposit.status | enum | Yes | Deposit remains unpaid until processor confirmation. Allowed: unpaid, paid. |
deposit.merchant_of_record | any JSON | Yes | The provider, never UQP. |
deposit.confirmation | object | No | Payment processor confirmation; client self-report is insufficient. Unknown properties are rejected. |
deposit.confirmation.source | any JSON | Yes | Payment confirmation must originate from the payment processor. |
deposit.confirmation.reference | string | Yes | Processor confirmation reference. Minimum length: 1. |
deposit.confirmation.confirmed_at | string | Yes | When the payment processor confirmed payment. Format: date-time. |
deposit.provenance | object | Yes | Source of settings or price, including who stated the price and when. Unknown properties are rejected. |
deposit.provenance.who | string | Yes | Who stated the price. Minimum length: 1. |
deposit.provenance.when | string | Yes | When the price was stated. Format: date-time. |
assumptions | array of string | Yes | Assumptions and limitations that accompany the estimate. |
intermediates | array of object | Yes | Ordered audit trail of every evaluation step, including disabled factors. Minimum items: 1. Item constraints: Unknown properties are rejected. |
intermediates[].step | string | Yes | Evaluator step name. Minimum length: 1. |
intermediates[].factor_id | string | Yes | Factor responsible for this step. Minimum length: 1. |
intermediates[].input | any JSON | Yes | Original input for the calculation; any JSON value. |
intermediates[].value | any JSON | Yes | Pack-defined value; any JSON value is permitted. |
intermediates[].skipped | any JSON | No | Pack-defined field. |
intermediates[].reason | string | No | Pack-defined field. |
review_status | enum | Yes | Current lifecycle state; allowed transitions must be enforced by the consumer. Allowed: QUOTED, ACCEPTED, DECLINED, EXPIRED. |
consent | object | Yes | Explicit research-sharing and anonymization preferences, with capture time. Unknown properties are rejected. |
consent.share_for_pricing_research | boolean | Yes | Whether sharing for pricing research was authorized. |
consent.anonymize | boolean | Yes | Requested anonymization preference, not proof that the artifact is anonymous. |
consent.captured_at | string | Yes | When consent was recorded. Format: date-time. |
anonymized | boolean | Yes | Whether the delivered artifact actually is anonymized, independently of the requested preference. |
valid_until | string | Yes | Binding Quote validity deadline; EXPIRED only past this timestamp. Format: date-time. |
kind | any JSON | Yes | Discriminator: estimate or quote. |
binding | any JSON | Yes | False for Estimate; true only for an artist-reviewed Quote. |
confidence | enum | Yes | Confidence in this estimate or reviewed offer; not a guarantee. Allowed: low, medium, high. |
client_approval | object | No | Explicit client approval required for ACCEPTED, matching the payment amount, currency and policies. Unknown properties are rejected. |
client_approval.amount | number | Yes | Nonnegative deposit amount. Minimum: 0. |
client_approval.currency | string | Yes | Three-letter uppercase currency code. Pattern: ^[A-Z]{3}$. |
client_approval.policies | array of string | Yes | Deposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1. |
client_approval.approved_amount | any JSON | Yes | Must be true to accept the payment amount. |
client_approval.approved_policies | any JSON | Yes | Must be true to accept the deposit policies. |
client_approval.approved_at | string | Yes | Timestamp of explicit client approval. Format: date-time. |
{
"kind": "quote",
"binding": true,
"confidence": "low",
"pack_id": "tattoo",
"pack_version": "0.1.0",
"answers": {
"description": "A small geometric mountain design with clean outlines.",
"size": "sm",
"placement": [
"Lower Arm"
],
"complexity": "simple",
"style": "blackgrey"
},
"artist_review_mode": "post_price",
"price_range": {
"min": 150,
"max": 250,
"currency": "USD",
"basis": "hourly",
"method": "rule",
"provenance": {
"who": "Example artist (synthetic review)",
"when": "2026-11-05T12:00:00Z"
}
},
"sessions": {
"count": 1,
"hours_per_session": 1
},
"deposit": {
"amount": 50,
"basis": "low",
"provenance": {
"who": "PricingModel tattoo@0.1.0 (example rules, unvalidated)",
"when": "2026-11-05T12:00:00.000Z"
},
"policies": [
"Example data, not real prices; provider must state final deposit policies before artist review."
],
"status": "unpaid",
"merchant_of_record": "provider"
},
"assumptions": [
"tattoo.co current constant, unvalidated; supplied handoff attributed to f018b86, not independently revalidated.",
"Pricing uses unrounded hours; display logs both Formula A Math.round(t*10)/10 and Formula B toFixed(1).",
"Sessions are a provisional single block (zero for by_piece); session splitting and caps are not modeled.",
"Multiple placements trigger any-tricky once; area summing is not modeled.",
"Raw answers are preserved; anonymized=false because free text has not been deidentified."
],
"intermediates": [
{
"step": "pricing_mode",
"factor_id": "pricing",
"input": "hourly",
"value": "hourly"
},
{
"step": "input_resolved",
"factor_id": "complexity",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": "simple"
},
{
"step": "input_resolved",
"factor_id": "style",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": "blackgrey"
},
{
"step": "input_resolved",
"factor_id": "size",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": "sm"
},
{
"step": "input_resolved",
"factor_id": "placement",
"input": {
"source": "asked",
"rule_id": null,
"keywords": []
},
"value": [
"Lower Arm"
]
},
{
"step": "input_resolved",
"factor_id": "tier",
"input": {
"source": "configured",
"rule_id": null,
"keywords": []
},
"value": "fallback_standard"
},
{
"step": "input_resolved",
"factor_id": "deposit",
"input": {
"source": "configured",
"rule_id": null,
"keywords": []
},
"value": "pct_of_low_30"
},
{
"step": "dimensions",
"factor_id": "size",
"input": {
"raw_size": "sm",
"width": null,
"height": null
},
"value": {
"width": 3,
"height": 3
}
},
{
"step": "area",
"factor_id": "size",
"input": {
"width": 3,
"height": 3
},
"value": 9
},
{
"step": "base_hours",
"factor_id": "size",
"input": {
"area": 9,
"area_per_hour": 12,
"minimum_hours": 1
},
"value": 1
},
{
"step": "placement",
"factor_id": "placement",
"input": {
"hours": 1,
"placements": [
"Lower Arm"
],
"mode": "additive_0_5h",
"match": "substring",
"tricky": false,
"adjustment": 0
},
"value": 1
},
{
"step": "complexity",
"factor_id": "complexity",
"input": {
"source": "asked",
"selected": "simple",
"hours": 1
},
"value": 1
},
{
"step": "complexity_hours",
"factor_id": "complexity",
"input": {
"multiplier": 1
},
"value": 1
},
{
"step": "skipped",
"factor_id": "wraparound",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 1,
"skipped": true,
"reason": "disabled"
},
{
"step": "hours_raw",
"factor_id": "hours",
"input": {
"pricing_mode": "hourly"
},
"value": 1
},
{
"step": "hours_estimate",
"factor_id": "hours",
"input": {
"raw": 1,
"decimal_places": 1,
"formula_a": 1,
"formula_b": "1.0"
},
"value": 1
},
{
"step": "tier",
"factor_id": "tier",
"input": {
"source": "fallback_standard",
"complexity": "simple",
"style": "blackgrey",
"rule_id": null,
"keywords": [],
"used": true
},
"value": "standard"
},
{
"step": "rate",
"factor_id": "rate",
"input": {
"pricing_mode": "hourly"
},
"value": {
"amount": 200,
"source": "tier:standard"
}
},
{
"step": "style",
"factor_id": "style",
"input": {
"source": "asked",
"selected": "blackgrey"
},
"value": 1
},
{
"step": "subtotal",
"factor_id": "pricing",
"input": {
"hours": 1,
"rate": 200,
"style_multiplier": 1
},
"value": 200
},
{
"step": "shop_minimum",
"factor_id": "minimum",
"input": {
"subtotal": 200,
"minimum": 200,
"applied": false
},
"value": 200
},
{
"step": "total",
"factor_id": "pricing",
"input": {
"by_piece": false
},
"value": 200
},
{
"step": "range_band",
"factor_id": "range",
"input": {
"total": 200,
"low_multiplier": 0.85,
"high_multiplier": 1.15
},
"value": {
"low": 170,
"high": 229.99999999999997
}
},
{
"step": "range",
"factor_id": "range",
"input": {
"low": 170,
"high": 229.99999999999997,
"rounding": {
"increment": 50,
"mode": "nearest"
}
},
"value": {
"low": 150,
"high": 250
}
},
{
"step": "deposit_base",
"factor_id": "deposit",
"input": {
"basis": "low",
"base": 150,
"percentage": 0.3
},
"value": 45
},
{
"step": "deposit_raw",
"factor_id": "deposit",
"input": {
"basis": "low"
},
"value": 45
},
{
"step": "deposit",
"factor_id": "deposit",
"input": {
"mode": "pct_of_low_30",
"unrounded": 45,
"increment": 25,
"minimum": 0
},
"value": 50
},
{
"step": "skipped",
"factor_id": "sessions",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "cover_up",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "multi_area",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "city_rate",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "skipped",
"factor_id": "non_usd",
"input": {
"skipped": true,
"reason": "disabled",
"question_ids": []
},
"value": 0,
"skipped": true,
"reason": "disabled"
},
{
"step": "sessions",
"factor_id": "sessions",
"input": {
"hours": 1,
"provisional": true
},
"value": {
"count": 1,
"hours_per_session": 1
}
},
{
"step": "expiry",
"factor_id": "expiry",
"input": {
"now": "2026-11-05T12:00:00.000Z",
"expiry_days": 7
},
"value": "2026-11-12T12:00:00.000Z"
}
],
"review_status": "QUOTED",
"consent": {
"share_for_pricing_research": false,
"anonymize": true,
"captured_at": "2026-11-05T12:00:00Z"
},
"anonymized": false,
"valid_until": "2026-11-12T12:00:00.000Z"
}Review and validation boundaries
Both pre_price and post_price review modes return a non-binding Estimate with a required price_range. Sessions and deposit remain required before review; provisional values must be explained in assumptions. Only artist review can issue a binding Quote.
Validate that min does not exceed max, that pack versions agree across documents and that questions and factors belong to the selected pack. JSON Schema does not execute pricing formulas or resolve model references.
Preserve raw answers such as size: "unsure". Never include contact details in answers. Known contact keys are rejected, but schema validation cannot find personal information inside arbitrary free text. Requested anonymization is not proof of deidentification; the Tattoo evaluator leaves anonymized false.
Lifecycle
The state names and every allowed edge below come from lifecycle.json. Consumers must enforce transitions: the schema validates state strings, while its transitions annotation is not an executable validation rule.
Text equivalent: every lifecycle transition
INITIATED→MANIFEST_ISSUEDMANIFEST_ISSUED→PAYLOAD_SUBMITTEDPAYLOAD_SUBMITTED→MUTATION_REQUIREDVALIDATED_PENDING_REVIEWMUTATION_REQUIRED→PAYLOAD_SUBMITTEDVALIDATED_PENDING_REVIEW→QUOTEDMUTATION_REQUIREDDECLINEDQUOTED→ACCEPTEDDECLINEDEXPIREDACCEPTED→ terminal; no outgoing transitionsDECLINED→ terminal; no outgoing transitionsEXPIRED→ terminal; no outgoing transitions
The repository’s spec/lifecycle.ts exports isLifecycleState, canTransition and transition. Illegal transitions throw. QUOTED requires artist review and a binding Quote with valid_until. ACCEPTED requires explicit client approval of the payment amount and policies before checkout handoff. EXPIRED is permitted only past valid_until. The intended checkout boundary is described under transports and checkout.
A price is always present
Every Estimate and Quote requires price_range with min, max and currency. Its basis is hourly, day_rate, from or fixed; method is artist_stated, rule or logged_quotes. Every price includes provenance: who stated it and when. logged_quotes requires n ≥ 5, enforced by the schema. A pending review never permits an absent price. Confidence and assumptions communicate uncertainty without inventing a final offer.
Motivation: UCP issue #877, “Catalog items with no public price (price on request)” describes the required catalog-price problem. This is an attributed issue report, not a normative UCP requirement or proof of UQP interoperability.
Two stages, then checkout handoff
INITIATED → MANIFEST_ISSUED → PAYLOAD_SUBMITTED returns the non-binding Estimate computed from PricingModel. Incomplete inputs lead to MUTATION_REQUIRED and resubmission; validated inputs lead to VALIDATED_PENDING_REVIEW. After artist review, QUOTED carries the binding Quote and valid_until. The client may explicitly approve the amount and policies to reach ACCEPTED, decline to reach DECLINED, or the offer becomes EXPIRED past valid_until. ACCEPTED is the handoff boundary, not a new handoff state.
Agents poll for status or receive webhooks; no long-held calls wait for artist review. The local reference uses polling; webhook delivery is deferred. UQP runs up to the binding Quote; payment is intended to use UCP or ACP checkout. No checkout adapter is implemented here.
A binding UQP Quote is intended to map 1:1 to the proposed dev.ucp.common.quote extension in UCP issue #929. That open proposal describes business-authoritative quotation on Cart and Checkout. Estimate is UQP’s pre-quote layer. This is an intended mapping to a proposal, not an implemented integration or UCP conformance claim.
Deposit and explicit approval
Deposit includes an amount, policies and provenance: who stated it and when. Before payment handoff, the client must explicitly approve that amount and those policies; merely submitting answers or viewing a quote is not acceptance. A deposit counts as paid only when the payment processor confirms it, for example through an authenticated processor webhook. Client self-report never completes payment. The provider is merchant of record; UQP never holds funds. Processor confirmation is not another quoting lifecycle state.
Versioning
The core is 0.1-draft; the Tattoo pack is 0.1.0. Pack version and core draft are separate. Every core object carries its selected pack_id and pack_version. Do not assume future drafts are backward compatible: pin a repository revision and pack version, and revalidate before updating.
No stable compatibility policy or transport negotiation contract is published yet. Track changes in the changelog and discuss schema changes through co-design and contributions.
Structure credit
The information architecture is inspired by Universal Commerce Protocol (UCP); the marketing structure is inspired by Shopify’s UCP page. UQP uses its own wording. This is structural inspiration, not an endorsement or a claim of implemented interoperability.