---
title: "Sending to outside systems"
url: https://services.dev/docs/service/submit
captured: [fallback.txt, legal-walk.txt, rejected.txt, release.txt, service-submit-fn.txt, submit.txt, tax-check.txt]
---
# Sending to outside systems

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

> A submit step sends the work to a system you don't control, waits for its answer and keeps a typed receipt. Rejections route by code, a timely repair stays timely, and silence falls back.

A submit step answers one question: did the outside system take it?

## Smallest form

```ts title="services/translation.ts"
    file: {
      submit: 'http://localhost:8787/registry',
      to: 'the registry',
      receipt: 'filing',
      rejected: { reject_once: 'correct', '*': 'translate' },
      perfect: '2d',
      fallback: 'hand_off',
    },
    correct: { ask: 'buyer', to: 'upload', routed: true },
    hand_off: { ask: 'buyer', to: 'answer', routed: true },
```

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

```txt title="magic values: none (metadata.sandbox.outside defaults to ack)"
services/translation.ts · 17 lines · $49 per job, within 48 hours, charged on file's receipt.
  who     AI delivers every job.
  steps   translate · ai → review · ai · adversarial → file · the registry → correct · buyer · upload · routed → hand_off · buyer · answer · routed
```

A rejection coded `reject_once` asks the buyer for a corrected file; any other code sends the work back to `translate`. Two rejections, or silence, hand the filing to the buyer.

## Every form

**On a submit step**

| Key | Written | Left out | Does |
|---|---|---|---|
| `submit` | `Url \| SubmitFn` | required | The outside system: its URL (on localhost with `remote: false`, the simulated one), or your function. |
| `to` | `string` | required | Its name on the record and the timeline. |
| `act` | `string` | the step name | The act this step performs, as the rules name it. |
| `receipt` | `<text output>` | required | The output field the receipt fills: its number, or the receipt's id; a list with `each`. `price.release` can name this step, or `<step>.accepted`. |
| `each` | `{ of: <list input> \| <list output>; lead?: (element) => boolean }` | none | Fan out: one submission and one receipt per element of a list. `lead` goes first; the rest wait for its acceptance. |
| `wait` | `Within` | `'2d'` | How long an answer may take. Silence past it goes to `fallback`. |
| `rejected` | `<step> \| { [code: string]: <step> \| { to: <step>; items?: (job, element) => AskItem[] } } \| ((codes, job) => <step>)` | none | Typed rejections: one repair step; a map from the outside system's codes to steps, `'*'` for the rest, where the order you write the codes in is their priority; or a function of every code the rejection carries. |
| `perfect` | `Within` | none | A repair resubmitted inside this window, counted from the first submission, keeps that submission's time. |
| `fallback` | `<step>` | none | After two rejections, or silence past `wait`: this step instead. Without one, the job stops at $0. |

**What the outside system answers, as your submit function returns it**

| Key | Written | Left out | Does |
|---|---|---|---|
| `status` | `ReceiptStatus` | required | `'transmitted'`, `'accepted'`, `'accepted_with_alerts'` or `'rejected'`. |
| `codes` | `string[]` | none | Its codes: why it rejected, or the alerts it accepted with. |
| `ref` | `string` | none | Its own number for the submission, such as an application number. The receipt field gets it, or the receipt's id. |
| `file` | `FileRef` | none | Its receipt as a file, kept in the job's documents plane by its sha256. |
| `fee_usd` | `number` | none | What it charged for the submission, in dollars, kept on the receipt. |

## A receipt for every submission

The send goes through [the gate](https://services.dev/docs/service/gate.md), so a guard can stop it. The job then waits `on: 'outside'`. The answer fills the `receipt` field, and the receipt is an object with its own file, stored by its sha256:

```bash
curl -s http://localhost:8787/jobs/job_7Hq2 | jq '.receipts[0]'
```

```json title="magic values: none (metadata.sandbox.outside defaults to ack)"
{
  "id": "rcp_7Hq2",
  "object": "receipt",
  "job": "job_7Hq2",
  "step": "file",
  "to": "the registry",
  "at": "2026-10-25T14:02:11.000Z",
  "status": "accepted",
  "livemode": false,
  "file": {
    "url": "http://localhost:8787/jobs/job_7Hq2/files/82d85d1a577fa13c7b2b5949e8b1aea6e5f307e492ad3a279c7618ab8cbb6956",
    "name": "rcp_7Hq2.txt"
  },
  "simulated": true
}
```

A receipt is `transmitted`, `accepted`, `accepted_with_alerts` or `rejected`, with the outside system's codes. Every file a job holds lives at `/jobs/:id/files/<sha256>`, so a file sent twice is stored once.

## No receipt, no charge

A Service with a submit step charges on its receipt. You don't write it: `check` says `charged on file's receipt`. Through a rejection and a repair, the buyer's money stays held:

```txt
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"release","metadata":{"sandbox":{"outside":"reject_once"}}}
```

```txt title="magic values: metadata.sandbox.outside = reject_once"
← job_7Hq2 · waiting · on party · $49.00 held
  money   charge.held → submission.sent → receipt.rejected
  ledger  AI $0.84 · you −$0.84
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
  money   charge.held → submission.sent → receipt.rejected → submission.sent → receipt.accepted → charge.collected
  ledger  +$49.00 · AI $0.84 · you $48.16
```

`price.release` can name `'<step>.accepted'` instead, so a `transmitted` answer isn't enough: the charge waits for `accepted` or `accepted_with_alerts`.

## Rejections route by code

`rejected` is one repair step, a map from the system's codes to steps, with `'*'` for the rest, or a function of the codes. A rejection is a move, not an error. Here the buyer repairs it a day later, through the link:

```txt
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"rejected","metadata":{"sandbox":{"outside":"reject_once"}}}
```

```txt title="magic values: metadata.sandbox.outside = reject_once"
← job_7Hq2 · waiting · on party · $49.00 held
  stage   rejected by the outside system · repairing
  receipt rcp_7Hq2 · rejected · reject_once · at 2026-10-25T14:02:11.000Z
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
  receipts rcp_7Hq2 · rejected · reject_once · at 2026-10-25T14:02:11.000Z
          rcp_7Hq3 · accepted · at 2026-10-25T14:02:11.000Z · sent 2026-10-26T14:02:11.000Z
```

## A timely repair stays timely

A repair resubmitted inside `perfect` keeps the first submission's time. The corrected file above went a day later, and its accepted receipt still says the 25th.

## Fallback

After two rejections, or silence past `wait`, the job goes to `fallback`. Here the registry never answers, the buyer files it themselves, and with no receipt the buyer pays nothing:

```bash
curl http://localhost:8787/jobs/job_7Hq2   # 2026-10-27: two days of silence
```

```txt title="magic values: metadata.sandbox.outside = never"
← job_7Hq2 · waiting · on party · $49.00 held
  record  translate · ai | review · ai · pass | file · the registry · silent past wait · fallback hand_off
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"answer":"Filed it myself at the counter."}
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 held
  record  translate · ai | review · ai · pass | file · the registry · silent past wait · fallback hand_off | hand_off · buyer · answer
→ POST /jobs/job_7Hq2/accept
← job_7Hq2 · accepted · lease.fr-CA.pdf · $0
  ledger  AI $0.84 · you −$0.84
```

The fallback is an ask, so `accept` closes the job at $0; the AI's $0.84 is the brand's.

## Your code as the outside system's client

Make `submit` a function and your code is the outside system's client: it sends one submission and returns the answer, built with `job.accepted(ref, { fee_usd, file, codes })`, `job.rejected(...codes)` or `job.transmitted(ref)`. `job.attempt` counts this step's submissions from 0, and `job.element` is the element with `each`:

```ts title="services/registered-translation.ts"
    file: {
      submit: async (_, job) => (job.attempt === 0 ? job.rejected('missing_certificate') : job.accepted('REG-2026-0042', { fee_usd: 12 })),
      to: 'the registry',
      receipt: 'filing',
      rejected: { missing_certificate: 'translate' },
    },
```

The registry's code becomes the AI's feedback, the redo is reviewed again, and the second submission's `ref` fills `filing`:

```txt
→ POST /registered-translation {"document":"deed.pdf","to":"fr","max_usd":59,"key":"registered"}
```

```txt title="magic values: none"
← job_7Hq2 · delivered · deed.fr.pdf · $59.00 charged
  record  translate · ai | review · ai · pass | file · the registry · rejected · missing_certificate · repairing at translate | translate · ai · redo | review · ai · pass | file · the registry · accepted · REG-2026-0042
  filing  REG-2026-0042
  ledger  +$59.00 · AI $1.68 · you $57.32
```

What your code, or a URL, can hand back, and what the job does with it:

| It answers | The job |
|---|---|
| `accepted`, or `accepted_with_alerts` with its `codes` | keeps the receipt, fills the receipt field with `ref` or the receipt's id, and goes on |
| `transmitted` | keeps the receipt, and the step counts the submission as filed: see [when the answer comes later](#when-the-answer-comes-later) |
| `rejected` with `codes` | moves to the repair the codes name; after two rejections, to `fallback` |
| nothing, a throw, a non-2xx or anything that isn't a receipt | silence, with the error on the record: past `wait`, the job goes to `fallback` |
| on a retry with `job.in_doubt`, the receipt the system already holds for `job.submission_id` | keeps it as if this send had made it: nothing is filed twice |

A URL gets `POST <url>` with a `services-job` header and the JSON body `{ job, step, to, attempt, element, submission_id, in_doubt, output }`, and answers the same receipt as JSON.

## When the answer comes later

Answer `transmitted` with their number as `ref`, and the step counts the submission as filed. The verdict arrives later by `receipt.answer`, from the system or your poller: `accepted` releases a charge on `<step>.accepted`, and a rejection routes by its code.

## Wire a real outside system

Beside `job.attempt` and `job.element`, your function reads the order's files with `text()`, and the job's output so far, its policy and its feedback.

- **One id per submission.** Send the outside system `job.submission_id`, `${job.id}:${job.element}:${job.attempt}`, so a retry is never filed twice: [a send in doubt](#a-send-in-doubt).
- **Map its codes.** Pass the system's own rejection codes to `job.rejected()`, and route them in `rejected`.
- **Silence is not a rejection.** Return nothing when it fails to answer: the job waits, and past `wait` goes to `fallback`. Return `rejected` only when the system said no.
- **Credentials.** Read the system's credentials from `process.env`, set from the brand's secrets once deployed. A credential the brand holds is a `Brand.credentials` entry.

[Federal first, then the states](https://services.dev/docs/examples/tax/efile.md) wires one end to end, with a transmitter the capture serves.

## A send in doubt

`job.attempt` counts repairs, never redeliveries, so a second wake sends under the same `submission_id`. A send that crashed before its receipt was recorded retries with `job.in_doubt: true`: ask the system for that id, and answer with the receipt it holds. apis.tax's e-file functions start that way:

```ts title="tax · lib/tax.ts"
const asked = (job: Filing) => (job.in_doubt ? taken.get(job.submission_id) : undefined)
  const held = asked(job)
  if (held) return held
```

## One submission per element

`each` sends one submission per element of a list, the lead first. A rejection in one never holds back the others, and its repair can re-ask only what its code names: [one submission per element](https://services.dev/docs/service/each.md).

## In the buyer's name

A submit step sends as the brand, under the brand's own credentials, and the receipt names the brand as the sender. What the outside system needs from the buyer, such as a signature or an authorization, an [ask](https://services.dev/docs/service/ask.md) collects first.

## Seven surfaces

| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
| `job.files.put` | `PUT /jobs/:id/files/:sha256`, `POST /jobs/:id/files` | `jobs files put <job> <path>` | never: a file is bytes: send it over HTTP or the SDK, and name it by URL in an order | `jobs.files.put()` |  | 2 error pages | customer portal, owner console |
| `job.files.get` | `GET /jobs/:id/files/:sha256` | `jobs files get <job> <sha256>` | never: a file is bytes: send it over HTTP or the SDK, and name it by URL in an order | `jobs.files.get()` |  | 1 error page | customer portal, owner console |

Objects: `rcp_`.
Events: `submission.sent`, `receipt.accepted`, `receipt.rejected`, `receipt.timed_out`.

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

## Refusals

| `check` gate | Why |
|---|---|
| [`release_unproven`](https://services.dev/docs/reference/check/release_unproven.md) | price.release names no step that runs. |

| Written | What TypeScript says |
|---|---|
| `input: { a: 'text' }, output: { filing: 'text' }, deliver: { draft: 'ai', file: { submit: 'https://registry.example.test/f', to: 'the registry', receipt: 'filing', rejected: 'drfat' } }, price: 49` | `Type '"drfat"' is not assignable to type '…'. Did you mean '"draft"'?` |

## 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/formation.ts · 30 lines"
    form_entity: { submit: 'https://filings.example.test/secretary-of-state', to: 'the state', receipt: 'file_number', rejected: { name_unavailable: 'rename' } },
```

```txt title="npx services.dev check · captured/legal-walk.txt"
services/formation.ts · 30 lines · $299 per job, within 48 hours, charged on form_entity's receipt.
  steps   consent · founders · consent → advise_entity · attorney · client · 1-item checklist → review · ai · adversarial → form_entity · the state → rename · founders · answer · routed → documents · ai → adopt · founders · sign → advise_83b · attorney · client · 1-item checklist
```

[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"
    file: { submit: efile, to: 'IRS and states', receipt: 'receipts', each: { of: 'jurisdictions', lead: j => j === 'US' }, rejected: { '*': 'prepare' }, when: job => job.input.entity !== 'partnership' },
```

```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: [Intake and request lists](https://services.dev/docs/service/intake.md)
- Next: [One submission per element](https://services.dev/docs/service/each.md)
