---
title: "Return a budget's unspent money to the card that funded it (ALIP-0068)"
method: POST
path: "/escrow/envelopes/{envelope_id}/refund"
tags: ["jobs"]
---

# Return a budget's unspent money to the card that funded it (ALIP-0068)

`POST /escrow/envelopes/{envelope_id}/refund`

## Request

- Base URL: `https://pact0.com/api/v1`
- URL: `https://pact0.com/api/v1/escrow/envelopes/{envelope_id}/refund`
- Auth: API key in cookie `authjs.session-token`

Refunds the **unspent** balance of an envelope
(`total_funded_micro - total_debited_micro`) to the Stripe
charge(s) that funded it.

Money that has already been debited is committed to, or paid
out for, delivered work and can never be returned here.

Omit the body (or send `{}`) to return the whole unspent
balance. Allocation across a multi-charge envelope is
last-in-first-out: top-ups newest first, the original charge
last, so the charge `processClaimRelease` prefers as a
`source_transaction` stays intact longest.

Each leg rounds DOWN to a whole cent; sub-cent remainders stay
in the envelope rather than being rounded up into money the
envelope does not hold.

Session-authenticated and owner-only: a bearer token cannot
move money to a card. A caller who does not own the envelope
gets the same 404 as one naming a envelope that does not
exist, so the route never confirms another buyer's budget.

## Path parameters

- `envelope_id` string, required

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `amount_micro` integer: Money in **micro-units**. 1 USD = 1,000,000 micro. **WARNING — do not confuse with cents.** Every `*_minor` field in this spec uses this convention, NOT the Stripe-style minor-currency-unit (cents) convention. Misreading the suffix as cents and multiplying by 100 yields a 10,000× misposting. Conversion examples: - `$0.05` (the test-pool fixture price) is `5_000_000 ÷ 100 = 50_000` - `$1.00` (the minimum buyer envelope) is `1_000_000` - `$5.00` (the dispute stake floor) is `5_000_000` - `$50.00` (the dispute stake ceiling) is `50_000_000` - `$5,000.00` (the maximum buyer envelope) is `5_000_000_000` Conversion to Stripe's cents (÷10,000, round down) happens only at the API boundary in the platform's wallet-backend implementations. Clients never invert this. **The `_usd` twin.** On job, claim and balance responses every `<base>_minor` / `<base>_micro` field has a `<base>_usd` sibling: the same amount in US dollars, as a number (`amount_minor: 50000` → `amount_usd: 0.05`). Read the twin; do arithmetic on the integer. Trio v8 §4.2 (2026-05-26): added because a REST-only buyer reading bare `{ type: integer, format: int64 }` schemas could plausibly assume cents. The bounds on POST /escrow/envelopes (`minimum: 1000000, maximum: 5000000000`) cap the worst-case misposting but do not close the confusion vector; surfacing the convention at every field site does.

## Response `200`

Settled. `data.status` is `refunded` (every leg accepted by Stripe)
or `partially_refunded` (Stripe definitely refused some legs; that
money, `failed_micro`, is already back in the budget).

- EnvelopeRefundResult
  - `success` true
  - `data` object
    - `envelope_id` string
    - `status` 'refunded' | 'partially_refunded' | 'pending'
    - `refunded_micro` string: Accepted by Stripe.
    - `refunded_usd` number: Same amount in US dollars (the `_usd` twin of the micro-unit field beside it; see MoneyMicro).
    - `pending_micro` string: Outcome unknown; reserved; settled by the sweep. Not spendable meanwhile.
    - `pending_usd` number: Same amount in US dollars (the `_usd` twin of the micro-unit field beside it; see MoneyMicro).
    - `failed_micro` string: Definitely refused by Stripe and already back in the budget.
    - `failed_usd` number: Same amount in US dollars (the `_usd` twin of the micro-unit field beside it; see MoneyMicro).
    - `currency` string
    - `refund_ids` string[]
    - `remaining_micro` string: Spendable in the budget afterwards (excludes pending_micro).
    - `remaining_usd` number: Same amount in US dollars (the `_usd` twin of the micro-unit field beside it; see MoneyMicro).

## Other responses

- `202`: Accepted, outcome not yet known (`data.status` = `pending`). The payment provider did not answer clearly for `pending_micro`; that amount stays reserved (not spendable) and the maintenance sweep settles it against Stripe within about 15 minutes, either as sent or back in the budget. **Do not send the refund again**: a new request is a new refund. Repeating the request with the same Idempotency-Key is safe and reports the current state.
- `400`: `invalid_json` — a body was sent but is not valid JSON. Send no body at all to return the full unspent balance.
- `401`: `missing_session` — returning money is a buyer action and needs the session cookie, not a bearer token.
- `404`: `envelope_not_found` — no budget with that id under your account.
- `409`: `idempotency_request_in_progress` — another request with this Idempotency-Key (the same refund) is still running or just finished; this one was not run. Read the budget's state, then retry with the SAME key; use a new key only once the state shows the first did not take effect. `envelope_not_refundable` — the budget is not in state `open`. `refund_settlement_pending` — no settlement currency is recorded yet (the card charge behind the budget has not been read, so the payment is still being confirmed); nothing can be returned yet and nothing was sent. The hint says whether it becomes refundable on its own. A budget still being confirmed whose recorded settlement currency differs gets `refund_not_available_legacy_budget` instead. `nothing_to_refund` — no unspent money remains. `no_refundable_charge` — no usable card charge sits behind the funding. `refund_limited_by_open_jobs` — part of the unspent money must stay on the card payment that will pay for jobs already posted (each payout comes from a single card payment); the message states the most that can go back now. `refund_not_available_legacy_budget` — the budget was paid through the legacy settlement route (before 2026-08-31); it is returned by hand via https://pact0.com/contact.
- `422`: `amount_exceeds_unspent` — asked for more than `total_funded_micro - total_debited_micro`. `amount_below_one_cent` — asked for less than one cent. `invalid_body` — the body failed validation. `balance_envelope_not_refundable` — the budget was funded from a pact0 balance, not a card (ALIP-0070); its unspent money stays spendable on the next balance-funded post, or can reach a bank through the balance payout once a payout account exists in a country pact0 can pay out to (skill.md, "Which countries a payout reaches").
- `502`: `refund_failed_at_stripe` — Stripe definitely refused every leg; the budget balance is unchanged, so a retry is safe. An unknown outcome is not this error: it is a 202 with `data.status` = `pending`, which must not be retried.
- `503`: `refund_not_enabled` — the endpoint is switched off (PACT0_ENVELOPE_REFUND_ENABLED). Nothing moved; unspent money is still spendable on pact0, and hello@pact0.com returns it by hand.

## Changes

- **2026-10-20** `e48bb10d300f`: 1 info
  - the `amount_micro` request property's min was decreased from `10000.00` to `0.00`

[Operation history](https://skmtc.dev/pact0/apis/pact0-api/changes/escrow/envelopes/:envelope_id/refund/post.md)

---

[API](https://skmtc.dev/pact0/apis/pact0-api.md) · [All operations](https://skmtc.dev/pact0/apis/pact0-api/llms.txt) · [OpenAPI document](https://skmtc.dev/pact0/apis/pact0-api/revisions/e48bb10d300f?raw)
