The customer portal

The running jobjob_7Hq2 · delivered · document.fr-CA.pdf · $49.00 charged

Every Service gets a portal for the people it works for, AI-only Services included. It is drawn from the operations a buyer, a client or a party calls, so every control on it is one of them. It opens on every date ahead, and shows each subject's periods, what it keeps on file and its clocks, the subscriptions and the allowances used.

The customer portal is where the people a job is for watch it move and do the few things only they can. You don't build it: dev serves it from your Service() at /portal, in your brand's words, and deploy puts it on your brand's domain.

Run
npx services.dev dev
captured
  buyers  http://localhost:8787/portal

Each screen is drawn from the operations registry rows that a buyer's key or delegated key, a client, or a party's own link can call. Each button is one of them, so the portal can't offer an act your API refuses.

All 55 operations
OperationDoesHTTP
service.orderOrder a job, or get the offer: the price and the exact next call. A Service whose price has every answers a subscription, sbn_….POST /jobs
service.quoteRead a Service as JSON, keyless; past the buyer key's cap, the checkout offer.GET /:service.json
service.waitlistJoin the waitlist of a Service that takes no orders yet: the next a not_live answer names.POST /:service/waitlist
service.listEvery Service, keyless: its price, when it charges and its status (live, preview or roadmap), and the metering rule every Service keeps: no record, no charge.GET /catalog
service.getOne Service of the catalog, keyless, by its id: its price, when it charges and its status.GET /catalog/:id
job.getRead a job, now or after waiting up to wait seconds while it works or waits on a person. Waiting on a party, it answers at once with the link to hand them.GET /jobs/:id
job.waitWait up to 50 seconds for a job to finish, through any person's line.GET /jobs/:id?wait=<seconds>
job.listThe buyer's jobs, newest first: OK, or EMPTY when none match.GET /jobs
job.acceptAccept a delivered job. It is final; a job nobody accepts is final 7 days after delivery.POST /jobs/:id/accept
job.send_backSend a job back with a note, and the AI redoes it: a delivered one as the buyer, a waiting one as the person at the line.POST /jobs/:id/send_back
job.cancelCancel a job before delivery. The buyer pays $0.POST /jobs/:id/cancel
job.endEnd a job at one of its ends, as that end's by allows. An end is forever.POST /jobs/:id/end
job.againOrder an ended job again, or a follow-on Service its end offers: a new job, with from on its record.POST /jobs/:id/again
job.policySet a job's policy values within their bounds: mechanics from the buyer or the brand's console, merits from the client alone.POST /jobs/:id/policy
job.signalRaise one of the Service's inbound signals. A signal never blocks; its goto moves the job.POST /jobs/:id/signals
job.messageSay something in the job's thread as the client. The AI answers at once, or holds advice a rules row reserves until a member adopts it.POST /jobs/:id/messages
job.threadRead the job's messages, oldest first: one resource with the posts. Held advice reads 'A professional is reviewing this answer.' until a member adopts it.GET /jobs/:id/messages
job.recordA job's record as you may read it, oldest first: a key reads the buyer's events of it, the owner the whole record. type=attested keeps the attestations: each step a person attested, under the rules version it pinned.GET /jobs/:id/record
subject.listThe subjects your jobs joined: each one keyed by an input or a receipt, with its jobs and who is of record on it.GET /subjects
subject.getRead a subject: its key, every job that touched it, its insert-only history, its kept facts with every value and the job that wrote it, its periods, and who is of record, or that the seat is vacant and who covers it.GET /subjects/:id
clock.listThe clocks your jobs and their subjects run: each one's date, who answers for it, its rungs and where it stands. A member reads the ones they answer for.GET /clocks
clock.getRead a clock: its date, who answers for it, each rung and when it falls due, what moved it, and at its date the owner's options.GET /clocks/:id
clock.ackAcknowledge a clock you answer for: inside its ack daily window, the day's acknowledgement, so the day is never missed. Only its owner can.POST /clocks/:id/ack
clock.satisfyClose a clock you answer for, on evidence on the record: a file you send, or a receipt or file the job holds. A recurring clock rolls on to its next date.POST /clocks/:id/satisfy
clock.chooseChoose one of a clock's lapse options at its date, unsatisfied, and the clock closes. A step's name runs that step, through the gate.POST /clocks/:id/choose
subscription.listYour subscriptions: each one's cycle, its allowances used and included, what it mints next, and the jobs it minted.GET /subscriptions
subscription.getRead a subscription: its cycle's start and end, each allowance used of included, each schedule date and when it next mints, each event it listens for, its charges, and every job it minted, offered or was refused.GET /subscriptions/:id
subscription.cancelCancel a subscription: it runs to the end of the cycle paid for, never renews, and nothing mints after. Twice changes nothing.POST /subscriptions/:id/cancel
ask.getRead an ask: its party, what it asks, its status and expiry, its items, and its link while it is open.GET /asks/:id
ask.completeThe party acts, through their own link: answer, upload, sign, consent, decide, or decline. An agent holding their delegated key may answer and upload; nothing else may act for them.POST /asks/:id
ask.revokeThe party revokes a grant they gave, through their own link: it stops being live at once, and a step that needs it is refused needs_grant until they grant it again. No key, delegated or not, revokes it for them.POST /asks/:id/revoke
ask.reissueReissue an open ask's link, to hand to its party again. The old one stops working at once.POST /asks/:id/reissue
job.files.listThe files a job holds, each by its sha256: the ones sent in, each step's artifacts and each receipt's file.GET /jobs/:id/files
job.files.putPut a file in a job's documents plane under its own sha256, or POST it and the brand hashes it. The same file sent twice is stored once.PUT /jobs/:id/files/:sha256
job.files.getRead a file a job holds, by its sha256: one sent in, a step's artifact, or a receipt's file.GET /jobs/:id/files/:sha256
key.createGet a test key: on localhost, the one dev printed, with its cap. Every call it makes is in the sandbox, and a live key is a person's to make, at checkout.POST /keys
key.raiseA person raises the buyer key's cap, at checkout.POST /keys/:key
test_clock.createMake a test clock frozen at a time. An order attaches to it with test_clock, and nothing on it moves until you advance it.POST /test_clocks
test_clock.advanceMove a test clock to a time, or by a span: every job on it fires what falls due, in time order across jobs. Moving it where it has been fires nothing.POST /test_clocks/:id/advance
event.listThe brand's events after a seq, in order, as the caller's audience sees them: a key reads the buyer's rows, the owner reads everything.GET /events
event.streamThe brand's events, pushed as they happen over server-sent events, from a seq or the Last-Event-ID; with watch=, that watch's deliveries.GET /events/stream
watch.createWatch events at a URL: each delivery is signed with the watch's whsec_ secret, shown once, and retried with backoff for three days.POST /watches
watch.listThe watches this key holds, with their deliveries.GET /watches
watch.deleteStop a watch. Deliveries still retrying stop with it.DELETE /watches/:id
watch.replayDeliver to a watch again every event it covers after a seq, in order.POST /watches/:id/replay
event.inboundSend an event in: a job's inbound signal by name; an event a subscription listens for, which mints the Service it routes to; or, on a test key, a synthetic event of the catalog, fired to its audience like any other.POST /events
brand.homeThe brand's home, as JSON and as Markdown: its name, what it sells and where to start. GET / is its page.GET /index.json
brand.healthWhether the brand answers: OK, with its version and how many Services it serves.GET /healthz
sandbox.fixturesThe sandbox's magic values: where each goes, the branch it takes, and its status. Each works on a test key.GET /fixtures
brand.cardThe brand's agent card: its Services, its access ladder, tools, links and probes.GET /.well-known/agents.json
brand.openapiEvery operation, as OpenAPI 3.1, with its webhooks.GET /openapi.json
brand.llmsWhat an agent reads first: the Services, and every operation.GET /llms.txt
brand.manifestEvery Service, as the manifest.GET /manifest.json
brand.pricingThe pricing document: calls are free, and only an order charges, at its Service's price. Every price, and the buyer key's hard ceiling.GET /pricing
brand.verifyThe brand's conformance suite: one case per operation row, live credential and magic value, with its auth, audience, replay and next cases, over HTTP and MCP. services.dev verify runs it.GET /verify/suite.json
PathShows
/portalComing up: every date ahead across your clocks, subscriptions, asks and grants, this week, the next 30 days and later. Outstanding: every open ask and request item across jobs, with its expiry, its link and where it came from, then each live grant. Your jobs, the ones that need you first, filtered by Service, state or what each waits on. Subjects, each with who is of record, its periods, what it keeps on file and its dates. Subscriptions, each allowance used of included. Money held and charged. Developers: the test key and its cap
/portal/jobs/:idYour turn, when the job waits on you. Its steps, who has the current one and when it's expected. The record, signed by name. What came back, with the evidence bundle. The acts the job allows. Policy. Thread

A button shows only while the job's state allows it:

ControlOperationShows when
Acceptjob.acceptdelivered
Send back, with what to fixjob.send_backdelivered
Cancel, under More: you pay $0, except work a person already attestedjob.cancelworking, or waiting on anything but your direction
End, under More, with what it costsjob.endan end the Service lets its buyer or client take, until accepted, canceled or ended
Order again, or the follow-on an end offersjob.againaccepted, canceled or ended
Set the policy, merits marked as the client's alonejob.policythe Service declares policy, until accepted, canceled or ended
Send, in the threadjob.messageworking, waiting or delivered
Send, in the Your turn bannerask.completethe turn is an answer or an upload; any other ask opens the party's own link
Acknowledge todayclock.acka clock you answer for is inside its daily acknowledgement
Mark it done, with a receipt the job holdsclock.satisfya clock you answer for is running
One button per option, under Due todayclock.choosea clock you answer for came due unsatisfied
Keep it with the steward Service, at its priceservice.ordera job ended with your date running, and the subject names a steward
I'll own this dateask.completethe same, through the link that job's end opened
Revokeask.revokea grant you gave is live
Cancel at the end of this cyclesubscription.cancela subscription is active

Translation, with no person in it, is the whole file:

TypeScript
import { Service } from 'services.dev'

export default Service({
  name: 'Translation',
  input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
  output: { document: 'file' },
  price: 49,
})

The AI delivers, and the screen opens on the buyer's turn: accept, or send it back with what to fix.

Call
→ POST /jobs {"service":"translation","document":"manual.pdf","to":"fr-CA","max_usd":49,"key":"portal"}
magic values: none
← job_7Hq2 · delivered · manual.fr-CA.pdf · $49.00 charged
Customer portal/portal/jobs/job_7Hq2job_7Hq2 · delivered at review · with the buyer

The legal example's patent application, at /portal/matters/:id in its brand's words. Ordered, the screen says when it's expected and what canceling costs:

Customer portal/portal/matters/job_7Hq2job_7Hq2 · working · with the ai

Waiting on the client, it opens on their turn, and the act runs through their own link, so no key or agent takes it:

Customer portal/portal/matters/job_7Hq2job_7Hq2 · waiting at consent · with the party

Delivered: the output, the filing's receipt, the evidence bundle and who signed:

Customer portal/portal/matters/job_7Hq2job_7Hq2 · delivered at file · with the buyer

The rest, in order, with advice held for review (Talk): the customer portal, worked.

Time fills five parts of /portal, each drawn from keys your Service already declares, so there is nothing to configure:

PartDrawn fromWhat the tax example declares
Coming upeach running clock's date, each date a subscription starts a job on, each open ask's expiry and each live grant's end, soonest firstclocks file, pay and expiring; on.schedule at Feb 1 and four estimate dates; the organizer's expires: '180d'
Subjects, in your word for themsubject: kind and key; one row per period, one job each; keeps, on file with the job that wrote it; of_record, named as their stamp names themkind: 'taxpayer', key: 'ein', period: 'tax_year', keeps carryforwards and tax, the signer of record
Dates, under each subjectclocks: owner says who answers, the next of rungs says when you'll hear. A clock whose owner is 'buyer' carries your controlspay: { owner: 'buyer' } offers Mark it done; file is the signer's
Outstandingeach open ask, its link and its request items, each saying where it comes fromthe organizer's items, read from last period's job
Subscriptionsprice.every and price.includes: each allowance used of included, and what one more costs once it's used upevery: 'year', includes: { 'annual-return': 1, estimate: 4, question: 12, 'notice-response': 2 }

On the tax example's test clock, a day after its subscription started the second period's job:

Customer portal/portaljob_4Tx3 · waiting at organizer · 1 outstanding · clk_7Hq4 open · clk_7Hq5 open · clk_7Hq6 open

Every date reads on the test clock the buyer's jobs ride, so you can walk a year of the portal before anyone buys.

When a job ends with one of your dates still running, the portal offers two ways to keep it: order the subject's steward Service, or sign that you'll own the date:

Customer portal/portalclk_7Hq2 open

Every control is also a call, so a buyer's agent can do the same over the API or MCP, except sign, consent, direct, decide, grant or raise the cap: those go to the person, by link.

  • Sign-in for the client's organization and its members
  • Receipts that name two sellers
  • The request log in the developer tools

The people who do the work have their own: the delivery portal. You have yours: the owner console.

Worked in: the sales customer portal.