---
title: "The customer portal"
url: https://services.dev/docs/app/buyers
captured: [app-lingua.txt, own-model.txt]
---
# The customer portal

URL: https://services.dev/docs/app/buyers

> 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.

```bash
npx services.dev dev
```

```txt title="captured"
  buyers  http://localhost:8787/portal
```

## What it derives from

Each screen is drawn from the [operations registry](https://services.dev/docs/reference/operations.md) 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.

| Operation | Does | HTTP |
|---|---|---|
| `service.order` | Order 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.quote` | Read a Service as JSON, keyless; past the buyer key's cap, the checkout offer. | `GET /:service.json` |
| `service.waitlist` | Join the waitlist of a Service that takes no orders yet: the next a not_live answer names. | `POST /:service/waitlist` |
| `service.list` | Every 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.get` | One Service of the catalog, keyless, by its id: its price, when it charges and its status. | `GET /catalog/:id` |
| `job.get` | Read 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.wait` | Wait up to 50 seconds for a job to finish, through any person's line. | `GET /jobs/:id?wait=<seconds>` |
| `job.list` | The buyer's jobs, newest first: OK, or EMPTY when none match. | `GET /jobs` |
| `job.accept` | Accept a delivered job. It is final; a job nobody accepts is final 7 days after delivery. | `POST /jobs/:id/accept` |
| `job.send_back` | Send 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.cancel` | Cancel a job before delivery. The buyer pays $0. | `POST /jobs/:id/cancel` |
| `job.end` | End a job at one of its ends, as that end's by allows. An end is forever. | `POST /jobs/:id/end` |
| `job.again` | Order 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.policy` | Set 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.signal` | Raise one of the Service's inbound signals. A signal never blocks; its goto moves the job. | `POST /jobs/:id/signals` |
| `job.message` | Say 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.thread` | Read 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.record` | A 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.list` | The 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.get` | Read 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.list` | The 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.get` | Read 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.ack` | Acknowledge 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.satisfy` | Close 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.choose` | Choose 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.list` | Your subscriptions: each one's cycle, its allowances used and included, what it mints next, and the jobs it minted. | `GET /subscriptions` |
| `subscription.get` | Read 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.cancel` | Cancel 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.get` | Read an ask: its party, what it asks, its status and expiry, its items, and its link while it is open. | `GET /asks/:id` |
| `ask.complete` | The 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.revoke` | The 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.reissue` | Reissue an open ask's link, to hand to its party again. The old one stops working at once. | `POST /asks/:id/reissue` |
| `job.files.list` | The 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.put` | Put 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.get` | Read 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.create` | Get 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.raise` | A person raises the buyer key's cap, at checkout. | `POST /keys/:key` |
| `test_clock.create` | Make 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.advance` | Move 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.list` | The 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.stream` | The 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.create` | Watch 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.list` | The watches this key holds, with their deliveries. | `GET /watches` |
| `watch.delete` | Stop a watch. Deliveries still retrying stop with it. | `DELETE /watches/:id` |
| `watch.replay` | Deliver to a watch again every event it covers after a seq, in order. | `POST /watches/:id/replay` |
| `event.inbound` | Send 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.home` | The 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.health` | Whether the brand answers: OK, with its version and how many Services it serves. | `GET /healthz` |
| `sandbox.fixtures` | The sandbox's magic values: where each goes, the branch it takes, and its status. Each works on a test key. | `GET /fixtures` |
| `brand.card` | The brand's agent card: its Services, its access ladder, tools, links and probes. | `GET /.well-known/agents.json` |
| `brand.openapi` | Every operation, as OpenAPI 3.1, with its webhooks. | `GET /openapi.json` |
| `brand.llms` | What an agent reads first: the Services, and every operation. | `GET /llms.txt` |
| `brand.manifest` | Every Service, as the manifest. | `GET /manifest.json` |
| `brand.pricing` | The 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.verify` | The 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` |

| Path | Shows |
|---|---|
| `/portal` | **Coming 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/:id` | **Your 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:

| Control | Operation | Shows when |
|---|---|---|
| Accept | [`job.accept`](https://services.dev/docs/reference/operations.md#jobaccept) | delivered |
| Send back, with what to fix | [`job.send_back`](https://services.dev/docs/reference/operations.md#jobsend_back) | delivered |
| Cancel, under More: you pay $0, except work a person already attested | [`job.cancel`](https://services.dev/docs/reference/operations.md#jobcancel) | working, or waiting on anything but your direction |
| End, under More, with what it costs | [`job.end`](https://services.dev/docs/reference/operations.md#jobend) | an end the Service lets its buyer or client take, until accepted, canceled or ended |
| Order again, or the follow-on an end offers | [`job.again`](https://services.dev/docs/reference/operations.md#jobagain) | accepted, canceled or ended |
| Set the policy, merits marked as the client's alone | [`job.policy`](https://services.dev/docs/reference/operations.md#jobpolicy) | the Service declares policy, until accepted, canceled or ended |
| Send, in the thread | [`job.message`](https://services.dev/docs/reference/operations.md#jobmessage) | working, waiting or delivered |
| Send, in the Your turn banner | [`ask.complete`](https://services.dev/docs/reference/operations.md#askcomplete) | the turn is an answer or an upload; any other ask opens the party's own link |
| Acknowledge today | [`clock.ack`](https://services.dev/docs/reference/operations.md#clockack) | a clock you answer for is inside its daily acknowledgement |
| Mark it done, with a receipt the job holds | [`clock.satisfy`](https://services.dev/docs/reference/operations.md#clocksatisfy) | a clock you answer for is running |
| One button per option, under Due today | [`clock.choose`](https://services.dev/docs/reference/operations.md#clockchoose) | a clock you answer for came due unsatisfied |
| Keep it with the steward Service, at its price | [`service.order`](https://services.dev/docs/reference/operations.md#serviceorder) | a job ended with your date running, and the subject names a steward |
| I'll own this date | [`ask.complete`](https://services.dev/docs/reference/operations.md#askcomplete) | the same, through the link that job's end opened |
| Revoke | [`ask.revoke`](https://services.dev/docs/reference/operations.md#askrevoke) | a grant you gave is live |
| Cancel at the end of this cycle | [`subscription.cancel`](https://services.dev/docs/reference/operations.md#subscriptioncancel) | a subscription is active |

## The smallest form

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

```ts
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](https://services.dev/docs/reference/operations.md#jobaccept), or [send it back](https://services.dev/docs/reference/operations.md#jobsend_back) with what to fix.

```txt
→ POST /jobs {"service":"translation","document":"manual.pdf","to":"fr-CA","max_usd":49,"key":"portal"}
```

```txt title="magic values: none"
← job_7Hq2 · delivered · manual.fr-CA.pdf · $49.00 charged
```

*Screen: the customer portal at `/portal/jobs/job_7Hq2`, job_7Hq2 · delivered at review · with the buyer (captured/screens/lingua-portal-ai.html).*

## One patent, from the client's side

The [legal example](https://services.dev/docs/examples/legal.md)'s patent application, at `/portal/matters/:id` in [its brand's words](https://services.dev/docs/service/brand-words.md). Ordered, the screen says when it's expected and what canceling costs:

*Screen: the customer portal at `/portal/matters/job_7Hq2`, job_7Hq2 · working · with the ai (captured/screens/legal-portal-ordered.html).*

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

*Screen: the customer portal at `/portal/matters/job_7Hq2`, job_7Hq2 · waiting at consent · with the party (captured/screens/legal-portal-consent.html).*

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

*Screen: the customer portal at `/portal/matters/job_7Hq2`, job_7Hq2 · delivered at file · with the buyer (captured/screens/legal-portal-delivered.html).*

The rest, in order, with advice held for review ([Talk](https://services.dev/docs/service/talk.md)): [the customer portal, worked](https://services.dev/docs/examples/legal/customer-portal.md).

## Dates, periods and allowances

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

| Part | Drawn from | What the tax example declares |
|---|---|---|
| **Coming up** | each running [clock](https://services.dev/docs/service/clocks.md)'s date, each date a subscription starts a job on, each open ask's expiry and each live [grant](https://services.dev/docs/service/grants.md)'s end, soonest first | clocks `file`, `pay` and `expiring`; `on.schedule` at `Feb 1` and four estimate dates; the organizer's `expires: '180d'` |
| **Subjects**, in your word for them | [`subject`](https://services.dev/docs/service/subject.md): `kind` and `key`; one row per [period](https://services.dev/docs/service/periods.md), one job each; `keeps`, on file with the job that wrote it; `of_record`, named as their stamp names them | `kind: 'taxpayer'`, `key: 'ein'`, `period: 'tax_year'`, `keeps` `carryforwards` and `tax`, the `signer` of record |
| **Dates**, under each subject | `clocks`: `owner` says who answers, the next of `rungs` says when you'll hear. A clock whose `owner` is `'buyer'` carries your controls | `pay: { owner: 'buyer' }` offers Mark it done; `file` is the signer's |
| **Outstanding** | each open ask, its link and its [request items](https://services.dev/docs/service/intake.md), each saying where it comes from | the organizer's `items`, read from last period's job |
| **Subscriptions** | `price.every` and `price.includes`: each [allowance](https://services.dev/docs/money/subscriptions.md) used of included, and what one more costs once it's used up | `every: 'year'`, `includes: { 'annual-return': 1, estimate: 4, question: 12, 'notice-response': 2 }` |

On [the tax example](https://services.dev/docs/examples/tax/customer-portal.md)'s test clock, a day after its subscription started the second period's job:

*Screen: the customer portal at `/portal`, job_4Tx3 · waiting at organizer · 1 outstanding · clk_7Hq4 open · clk_7Hq5 open · clk_7Hq6 open (captured/screens/tax-portal-year.html).*

Every date reads on the [test clock](https://services.dev/docs/reference/operations.md#test-clocks) 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](https://services.dev/docs/reference/operations.md#serviceorder) the subject's steward Service, or sign that you'll [own the date](https://services.dev/docs/reference/operations.md#askcomplete):

*Screen: the customer portal at `/portal`, clk_7Hq2 open (captured/screens/app-buyer-clocks.html).*

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](https://services.dev/docs/mcp/never.md): those go to the person, by link.

## Not on its screens yet

- 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](https://services.dev/docs/app/members.md). You have yours: [the owner console](https://services.dev/docs/app/owner.md).

Worked in: [the sales customer portal](https://services.dev/docs/examples/sales/customer-portal.md).

---

- Up: [Your first paid job](https://services.dev/docs.md)
- Previous: [A buyer that is software](https://services.dev/docs/sdk/agent.md)
- Next: [The delivery portal](https://services.dev/docs/app/members.md)
