UI contracts and the headless-parity test
Open in
The UI contracts are what any screen renders: six answers for every principal and record, generated from the file. The headless-parity test has an agent finish every path a UI offers with no UI.
Filled: the six answers, the same for every reader. Hollow: what writes them and what reads them.
Capability families
APIs.Dev specifies no App, client portal or delivery screen. It specifies what each one reads, so any UI, from any builder, renders the product without a private API. The landing site, the developer portal and the docs are specified as products; every other UI gets these contracts.
| Family | What it gives every product |
|---|---|
| The affordance contract | Six answers for any principal and record: the state; what they may see; the moves allowed, each with its input schema and price; the moves refused, each with its cure; who else can act; and a live subscription |
| The app map | What a member can reach, in order: the Nouns, views and settings their Role opens, generated from the file |
| Views | Saved filters, sorts and groupings as data, each answered over every Interface with its count |
| Forms | Per Verb: fields, order, defaults, validation and help, from its input schema. A form never asks for what the Verb does not take |
| Settings | The account's own settings as Verbs: members, Roles, plan, Integrations, preferences |
| Onboarding as data | The first steps as a checklist of Verbs, each ticked when its Event arrives, the same for a person and an agent |
| Branding tokens | Name, logo, colours and type from brand, for any UI, email and receipt |
| A reason for every empty state | Every empty list or view says why (none yet, filtered out, hidden by Role, not in the plan) and what fills it |
| Contracts per pattern | Member contracts (Headless SaaS, Content), client and delivery contracts (Service API), storefront and supplier contracts (Marketplace): the six answers, plus the pattern's records |
| The headless-parity test | Every path a UI offers, finished by an agent through the API or MCP and kept in the proof record. A UI that has to invent a state, a move or a reason fails it, and marks a capability the product lacks |
The six paths
The suite runs each path at every deploy, in the Test twin, with an agent and no UI.
| Path | Who | What it covers |
|---|---|---|
| An admin's setup | A Customer's admin | Create the org, invite, set Roles, connect single sign-on and an Integration |
| A member's day | A member | Create, relate, search, move a record, restore one, export |
| A client's matter | A buyer, or its agent | Order, approve the quote, the reserved act by link, accept or contest, pay |
| A worker's shift | A worker | Claim a task, complete it, hand it off, mark a booking attended |
| A supplier's order | A supplier, or its agent | Apply, pass verification, publish an item, claim, fill, read the payout |
| A buyer's purchase | Another product's agent | Find, evaluate, pay, invoke, prove, contest |
Lifecycles and states
| Thing | States | What moves it |
|---|---|---|
| A contract, per principal and record | current, then changed and current again | The record's state, the principal's Role, entitlement or mandate, or a new Version; each change goes out on the subscription |
| A live subscription | open, behind, closed | The client; one that falls behind resumes from its last seq |
| An onboarding checklist | started, then complete or dismissed | Each step's Event; the member |
| A path in the suite | defined, running, then passed or failed | Each deploy in the Test twin; a failure names the step and the missing capability |
| A UI's conformance | checked, then conforming or inventing | The check of each screen against the contracts; inventing names the screen and what it made up |
A contract is never cached past a change: the move that changes a record's state changes every principal's allowed moves in the same commit.
What you declare, what the platform does
| You declare | The platform does |
|---|---|
| Nothing for the six answers | Builds them for every principal and record from the Nouns, lifecycles, Roles, Offers and mandates, and serves them on every Interface |
views | Answers each as data with its count, masked by Role, and gives each empty answer its reason |
ui.app | Orders the app map; each member sees only what their Role opens |
ui.forms | Sets field order and help; the rest comes from each Verb's schema |
ui.onboarding: a list of Verbs | Keeps a checklist per Customer, ticked by Events |
ui.empty: a reason per view | Serves it with an empty answer; reasons from Roles and plans come without a line |
brand | Serves the tokens to any UI, and applies them to every email and receipt |
parity: paths beyond the six | Runs them with the six at every deploy, and refuses a deploy that breaks one |
Example
pipeline names its app map, a form, onboarding and the reason its open view can be empty.
import { define } from 'apis.dev'
/** Deals for each Customer, kicked off when won. */
export default define({
name: 'pipeline', domain: 'pipeline.apis.dev',
roles: { rep: { see: { Deal: 'own' } }, manager: { has: 'rep', see: { Deal: 'all' } } },
Company: { name: 'string', domain: 'url!#', deals: '<-Deal.company' },
Deal: {
title: 'string', amount: 'decimal(12, 2)', company: '->Company.deals',
stage: { open: [], qualify: 'opened', lose: ['opened', 'qualified'],
win: { in: 'qualified', role: 'manager', approve: { above: { amount: 50000 } } } },
},
views: { open: { Deal: { stage: ['opened', 'qualified'] }, sort: '-amount' } },
ui: {
app: ['views.open', 'Company', 'settings'], // the app map
forms: { 'Deal.open': ['title', 'amount', 'company'] },
onboarding: ['Company.create', 'Deal.open', 'crm.connect'],
empty: { open: 'No open Deals. Open one, or connect your CRM to bring yours in.' },
},
offer: { Team: { month: 49, seats: 5, seat: 9 }, 'Deal.open': 0.001 },
})Dana is a rep. Her contract for a qualified Deal of 64,000: she may lose it but not win it, and a manager's win waits for approval.
{
"record": "deal_1",
"state": "qualified",
"see": { "fields": ["title", "amount", "company", "stage"], "hidden": [] },
"allowed": [
{ "verb": "Deal.lose", "price": 0,
"input": { "type": "object", "properties": { "reason": { "type": "string" } } } }
],
"refused": [
{ "verb": "Deal.win", "code": "role_required",
"cure": "A manager wins a Deal: sam@acme.com" }
],
"others": [
{ "principal": "sam@acme.com", "role": "manager", "can": ["Deal.win", "Deal.lose"],
"pending": { "Deal.win": "approval_required" } }
],
"live": "/deals/ deal_1/ live",
"seq": 41
}The suite runs at deploy, and checks a screen built on the product against the same contracts.
$ npx apis.dev test --parity
✓ an admin's setup 9 moves over MCP, no UI
✓ a member's day 14 moves over MCP, no UI
✕ screen 'Deals board', column 'stalled': no state, view or metric defines itA stalled view in the file fixes it; a column that only the screen knows does not ship.
Contracts with the other subsystems
The contracts decide nothing. Each answer is a question the other subsystems already answer, asked for one principal and one record.
| Subsystem | What the contracts take from it | What they give it |
|---|---|---|
| Definition and compile | Nouns, lifecycles, views, ui and brand, checked at deploy: an app map entry or onboarding step that names no Verb or view is refused | The paths, run in the Test twin at every deploy |
| Identity, access and policy | The one decision, asked for every move; masks per field and record; who else holds a Role or a mandate | Nothing new: each refused move carries the decision's own cure |
| Model and records | The record, its state and history; views and metrics as projections | Reads that never show what the Role hides, with the reason |
| Runtime | Events and live subscriptions | The seq a client resumes from |
| Work | Tasks, turns and the seen states of a case | The client, delivery and supplier contracts a screen renders |
| Commerce | The price of each move, entitlements, the plan's limits | A refused move's cure: the plan or the payment that lifts it |
| Interfaces and distribution | Every Interface, so the contract reads the same over the API, MCP and the SDK | The in-app channel, rendered from the record's conversation |
| Trust and operations | The proof record | Each path's result per Version, and each UI's conformance |
What Customers and their agents meet
- A screen that cannot lie. Every button is an allowed move, every greyed one carries its cure, and every empty list says why.
- No private API. The App, a client portal and a supplier's screen read the contracts an agent reads. Anything a UI shows, an agent can fetch.
- An agent that finishes the job. An agent reads the contract, makes the allowed move, and follows the subscription. It never needs a screen.
- Onboarding that counts what happened. A checklist ticks itself when its Event arrives, whether a person or an agent made the move.
- Your brand everywhere. The same tokens style a UI, an email and a receipt.
- The same answer everywhere. A refusal on a screen, over MCP and in a 403 carries the same code and the same cure.
Any UI is a view of the contracts, so whatever a person can do on a screen, an agent can do without one.
Transactional channels
Email, SMS, chat and voice that deliver what was bought and answer what was asked, in one conversation per record, with consent per channel.
What every product gets
Everything around your API, live on your own domain from one deploy: CLI, MCP, SDKs, docs, a landing site, a portal and Stripe billing.