---
title: "Asks: a ceremony without an account"
url: https://services.dev/docs/service/ask
captured: [ask.txt, grow-ask.txt, hand-to-person.txt, legal-walk.txt, rejected.txt, service-consent.txt, service-lapse.txt, service-sign.txt, tax-check.txt]
codes: [ask_expired, ask_voided, needs_the_party, signature_void]
---
# Asks: a ceremony without an account

URL: https://services.dev/docs/service/ask

> An ask step waits on a party to sign, consent, answer, upload, decide or direct, through a single-use link that needs no account. Only that party can complete it.

An ask answers one question: what does this job need from someone outside the brand, and who alone can give it?

## Smallest form

```ts title="grow/ask.ts"
    glossary: { ask: 'buyer', to: 'answer', items: () => [{ id: 'terms', label: 'Which terms stay in English?' }] },
```

```txt
→ POST /translation {"document":"lease.pdf","to":"fr-CA","key":"grow","max_usd":49}
```

```txt title="captured/grow-ask.txt · magic values: none"
← job_7Hq2 · waiting · on party · $49.00 held
  waiting {"on":"party","since_at":"2026-10-25T14:02:11.000Z","step":"glossary","clock":"paused"}
```

## Every form

**On an ask step**

| Key | Written | Left out | Does |
|---|---|---|---|
| `ask` | `'buyer' \| <party input>` | required | Who acts: the buyer, or a `party`, `party?` or `party[]` input. |
| `to` | `AskTo` | required | What the party does: `'sign'`, `'consent'`, `'answer'`, `'upload'`, `'decide'`, `'direct'` or `'grant'`. |
| `names` | `RoleWord` | none | For `direct`: the role whose next Task the client locks to the one member they name. |
| `what` | `<step>` | the step before | For `sign`: the step whose output is signed. The signature binds that output's hash. |
| `all` | `true` | any one | For a `party[]` input: every party acts, and the ask is done when the last one has. |
| `how` | `'click' \| 'esign' \| 'id-verified' \| 'notarized'` | `'click'` | How the act is proven, recorded with it. |
| `capacity` | `string \| string[]` | none | The capacity the party acts in, printed on the record: 'officer', 'partner', 'trustee'. |
| `void` | `{ tolerance: { <output>: number } }` | none, so any change voids it | For `sign`: the change each output field may take and keep the signature. A number is the most it may move. |
| `items` | `(job) => AskItem[]` | none | For `answer` and `upload`: a request list derived from the job. A step can start on one item: `after: '<ask>.<item>'`. |
| `choices` | `string[]` | none | For `decide`: the choices. A decision is merits, the client's alone. |
| `scope` | `{ acts: string[]; for: Within }` | live until revoked | For `grant`: the acts it permits, and how long it stays live once given. Its state (live, expired, revoked) comes from the record; the party revokes it through their own link. |
| `expires` | `Within` | `'7d'` | When the link stops working; a second expiry lapses the job. |
| `remind` | `'${number}d'[]` | [`'1d'`] | Reminders, each this long before the link expires. |

**Each item of a request list**

| Key | Written | Left out | Does |
|---|---|---|---|
| `id` | `string` | required | Its name: a step can start on it with `after: '<ask>.<item>'`. |
| `label` | `string` | required | The question or the document asked for, as the party reads it. |
| `optional` | `true` | none | The ask is done without it. |
| `remind` | `'${number}d'[]` | none | Reminders for this item alone, each this long before the link expires, while the item is still open. |
| `from` | `string` | none | Where it comes from, as the party reads it: `on the 2025 return`. The customer portal shows it beside the item. |

**What the party sends: `POST /asks/:id`, through their link**

| Key | Written | Left out | Does |
|---|---|---|---|
| `answer` | `unknown` | none | An `answer` ask's answer. |
| `file` | `unknown` | none | An `upload` ask's file. |
| `items` | `Record` | none | A request list's items: each answered (`answer` or `file`), waived with a reason, or substituted. |
| `choice` | `unknown` | none | A decide ask's choice. |
| `member` | `unknown` | none | A direct ask's member: the one member of its role the client names. |
| `end` | `unknown` | none | A direct ask on a declined job: the end the client chooses instead of another member. |
| `decline` | `unknown` | none | The party says no. A decline ends the job at `declined`. |
| `reason` | `unknown` | none | Why, with a decline. |

## An ask is an object

Each ask has an id, a status (`open`, `done`, `expired` or `voided`), an expiry and its own link on the brand's domain:

```bash
curl http://localhost:8787/asks/ask_7Hq2
```

```json title="captured/ask.txt · magic values: none"
{
  "id": "ask_7Hq2",
  "object": "ask",
  "job": "job_7Hq2",
  "step": "glossary",
  "to": "answer",
  "party": "buyer",
  "what_hash": null,
  "status": "open",
  "expires_at": "2026-11-01T14:02:11.000Z",
  "url": "http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_",
  "livemode": false,
  "simulated": true,
  "items": [
    {
      "id": "terms",
      "label": "Which terms stay in English?",
      "status": "open"
    }
  ]
}
```

The party opens the link and acts. They need no account, and the link works once. Behind the page is one call, `POST /asks/:id` with the link's token, and its body is the last table in [Every form](#every-form):

```txt
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}
```

```txt title="captured/ask.txt · 200 · ask_7Hq2 · done · magic values: none"
← 200 · ask_7Hq2 · done
```

While the job waits on them, its deadline pauses, and the record says for how long:

```bash
curl http://localhost:8787/jobs/job_7Hq2?wait=50
```

```txt title="captured/ask.txt · magic values: none"
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
  record  glossary.terms · answered | glossary · buyer · answer · clock paused 2h 5min | translate · ai | review · ai · pass
```

Each item can be answered, waived with a reason or substituted, and a step that needs only one item can start on it with `after: 'glossary.terms'`.

## Only the party

An agent can carry the link to its person. It can't act for them, and neither can the brand. Over MCP the job's next step is `hand_to_person`, with the url. An agent that tries to finish the job itself is refused, and the refusal hands it the link again:

```txt
→ tools/call accept_job {"id":"job_7Hq2"}
```

```txt title="captured/hand-to-person.txt · magic values: none"
← {
  "code": "needs_the_party",
  "reason": "glossary waits on the buyer to answer. Only they can, through their own link. Hand them the link in next.",
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_",
  "doc_url": "http://localhost:8787/docs/errors/needs_the_party",
```

The brand's own console gets the same no:

```txt
→ POST /owner/jobs/job_7Hq2/approve
```

```txt title="captured/service-lapse.txt · 403 · needs_the_party · sign waits on Ada Lovelace to sign. Only they can, through their own link. Hand them the link in next. · magic values: client.email = silent@example.test"
← 403 · needs_the_party · sign waits on Ada Lovelace to sign. Only they can, through their own link. Hand them the link in next.
```

An `answer` or `upload` ask may also be completed by an agent holding the party's delegated key, once delegated keys ship. A sign, a consent or a decision is always the party's own.

## Sign binds what was signed

A signature binds the hash of the step it signs. `void.tolerance` says how far each field may move and keep it. Typesetting adds a page and the signature holds; it adds three, and the client signs again before delivery:

```ts title="services/signed-translation.ts"
    sign: { ask: 'client', to: 'sign', what: 'translate', how: 'esign', void: { tolerance: { pages: 1 } } },
```

```txt
→ POST /signed-translation {"document":"lease.pdf","to":"fr","client":{"name":"Ada Lovelace","email":"signs@example.test"},"added":1,"max_usd":120,"key":"added-1"}
```

```txt title="captured/service-sign.txt · magic values: client.email = signs@example.test"
← job_7Hq2 · delivered · lease.fr.pdf · $120.00 charged
  record  translate · ai | harden · ai · adversarial · 2 lenses · pass | sign · client · sign · esign | typeset · code
  pages   1: typesetting added 1, and the signature holds a change of 1
  asks    ask_7Hq2 · done · sha256:0f613db007970da2596d27f5be43688dec330d5118ee7d1cbc1482befc98ec46
← job_7Hq3 · delivered · lease.fr.pdf · $120.00 charged
  record  translate · ai | harden · ai · adversarial · 2 lenses · pass | sign · client · sign · esign | typeset · code | sign · signature void | sign · client · sign · esign
  pages   3: typesetting added 3, and the signature holds a change of 1
  asks    ask_7Hq3 · voided · sha256:0f613db007970da2596d27f5be43688dec330d5118ee7d1cbc1482befc98ec46
          ask_7Hq4 · done · sha256:254af4bffb58225dda08346ed8b56de950d74f7013a2258a1cce44dc260a0587
```

Each ask keeps `what_hash`, the hash it binds. The voided ask bound the work before typesetting; the new one binds it as delivered.

## Consent and decide

A consent comes before the work it covers. A decision is the client's choice among `choices`, and it is merits: once committed, [the gate](https://services.dev/docs/service/gate.md) never undoes it on mechanics.

```txt
→ POST /private-translation {"document":"memo.pdf","to":"de","client":{"name":"Ada Lovelace","email":"signs@example.test"},"max_usd":59,"key":"consents","metadata":{"sandbox":{"asks":{"register":"informal"}}}}
```

```txt title="captured/service-consent.txt · magic values: client.email = signs@example.test, metadata.sandbox.asks = { register: informal }, client.email = declines@example.test"
← job_7Hq2 · delivered · memo.de.pdf · $59.00 charged
  record  consent · client · consent | register · client · decide · decided informal | translate · ai | review · ai · pass
  ledger  +$59.00 · AI $0.84 · you $58.16
← job_7Hq3 · ended · declined · $0
  record  declined · ended · Ada Lovelace declined consent.
  ledger  you $0.00
```

A party who declines ends the job at `declined`, and only paid acts already done are owed.

## Upload

An upload ask takes a file, and a repair can be one. When the outside system rejects a filing, the buyer uploads the corrected file through the link:

```bash
curl http://localhost:8787/jobs/job_7Hq2
```

```txt title="captured/rejected.txt · magic values: metadata.sandbox.outside = reject_once"
  record  translate · ai | review · ai · pass | file · the registry · rejected · reject_once · repairing at correct | correct · buyer · upload · clock paused 1d | file · the registry · accepted · rcp_7Hq3
```

## Expiry, reminders and lapse

A link expires after 7 days, with a reminder a day before. An expired ask opens once more, and a second expiry ends the job at `lapsed`:

```bash
curl http://localhost:8787/owner/jobs/job_7Hq2   # 2026-11-08: the second ask expires
```

```txt title="captured/service-lapse.txt · magic values: client.email = silent@example.test"
← job_7Hq2 · ended · lapsed · lease.fr.pdf · $0
  record  translate · ai | harden · ai · adversarial · 2 lenses · pass | sign · reminded 1d before expiry | sign · expired | sign · reminded 1d before expiry | sign · expired | lapsed · ended · clock · sign expired twice.
```

`POST /asks/:id/reissue` sends a fresh link and voids the old one.

## Direct and grant

`to: 'direct'` lets the client name the one member who takes a role's next Task: `{ ask: 'client', to: 'direct', names: '<role>' }`. A member's decline opens one on its own, and the job waits `on: 'direction'` until the client answers through their link: [declines and direction](https://services.dev/docs/people/direction.md) walks it, from the decline to the member the client names. `to: 'grant'` is a scoped, expiring permission a step `needs`, and only the party revokes it: [Grants and decisions](https://services.dev/docs/service/grants.md). A request list for `answer` and `upload` asks has its own page: [Intake and request lists](https://services.dev/docs/service/intake.md).

## Seven surfaces

| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
| `ask.get` | `GET /asks/:id` | `asks get <ask>` | never: a person acts | never: a person acts |  | 1 error page | hosted link, owner console |
| `ask.complete` | `POST /asks/:id` |  | never: a person acts | never: a person acts |  | 6 error pages | hosted link |
| `ask.reissue` | `POST /asks/:id/reissue`, `POST /asks/:id/link` | `asks reissue <ask>` | `reissue_ask` | `asks.reissue()` |  | 3 error pages | customer portal, owner console |

Objects: `ask_`.
Events: `ask.opened`, `item.done`, `ask.reminded`, `ask.done`, `ask.expired`, `ask.voided`, `ask.relinked`, `consent.given`, `job.paused`, `job.ended`.

[Operations](https://services.dev/docs/reference/operations.md) has every column of every row.

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`needs_the_party`](https://services.dev/docs/reference/errors.md#needs_the_party) | blocked | 403 | Only the party can do this, through their own link. |
| [`ask_expired`](https://services.dev/docs/reference/errors.md#ask_expired) | conflict | 410 | This ask expired. |
| [`ask_voided`](https://services.dev/docs/reference/errors.md#ask_voided) | conflict | 410 | This link no longer works: used, reissued or voided. |
| [`signature_void`](https://services.dev/docs/reference/errors.md#signature_void) | conflict | 409 | What was signed changed past its tolerance. |

| Written | What TypeScript says |
|---|---|
| `input: { applicant: 'party', document: 'file' }, output: { document: 'file' }, deliver: { sign: { ask: 'document', to: 'sign' } }, price: 49` | `Type '"document"' is not assignable to type '"applicant" \| "buyer" \| undefined'.` |
| `input: { client: 'party' }, output: { document: 'file' }, deliver: { authorize: { ask: 'client', to: 'grant', scope: { acts: ['file'], for: '30d' } }, file: { by: 'ai', needs: 'authorise' } }, price: 49` | `Type '"authorise"' is not assignable to type '"authorize" \| "file" \| undefined'. Did you mean '"authorize"'?` |

## In the example brands

The lines that use it in the example brands, as their files write them, and what `check` read aloud for each file.

```ts title="legal · services/contract-review.ts · 16 lines"
    consent: { ask: 'client', to: 'consent', expires: '7d' },
```

```txt title="npx services.dev check · captured/legal-walk.txt"
services/contract-review.ts · 16 lines · $99 per job, within 48 hours, charged on acceptance.
  steps   consent · client · consent → flag · ai → attack · ai · adversarial → advise · attorney · client · stamped · 2-item checklist → send · ai
```

[The legal example](https://services.dev/docs/examples/legal.md) runs every one of its files, captured.

```ts title="tax · services/amended-return.ts · 22 lines"
    authorize: { ask: 'owner', to: 'sign', what: authorization, how: 'id-verified', capacity: 'officer', void: { tolerance: { balance_due: 1 } } },
```

```txt title="npx services.dev check · captured/tax-check.txt"
services/amended-return.ts · 22 lines · $900 per job, within 30 days, charged when file or aar is accepted.
  steps   compute · code · cites depreciation and carryforward schedules → draft · ai · cites the workpapers and the records the organizer took → check · ai · adversarial · 2 lenses → prepare · preparer · * · 1-item checklist → sign · signer · * · stamped in the paid-preparer block → authorize · owner · sign → file · IRS and states, unless a partnership → aar · IRS · Form 8082, for a partnership · emits k1.adjusted
```

[The tax example](https://services.dev/docs/examples/tax.md) runs every one of its files, captured.

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [Review steps](https://services.dev/docs/service/review.md)
- Next: [Grants and decisions](https://services.dev/docs/service/grants.md)
