# You queued the wrong file, and the money has not left yet

> While a job is still waiting its turn, cancelling it gives the credit back. Once an engine has started, it cannot, and the tool says so.

Canonical HTML version: https://mcpbelt.com/problems/cancel-a-job-that-has-not-started

## The problem

The agent queued an hour of audio and it was the wrong hour of audio. Or it was the right one, and the person paying changed their mind thirty seconds later. Either way the job is sitting in a queue, nothing has happened to it yet, and money is about to be spent on a result nobody wants.

This is a normal amount of wrong. An agent working from an instruction picks a file from a list, and lists have neighbours. The mistake is cheap to make and, in most setups, impossible to take back.

What you want in that moment is narrow and specific: stop the job that has not started, get the credit back, and be told plainly if you were too late. Not a best effort, not a maybe.

## Why it fails locally

With a provider API there is usually nothing to cancel, because there was never a queue. You made a call, the call is running, and the charge was incurred the moment the request was accepted. There is no gap to cancel inside.

What is often called cancelling is closing the connection, and that only stops you listening. The work continues on the other side and is billed in full: you have thrown away the result you paid for, which is worse than not cancelling. On the long-job APIs that do have a cancel endpoint, the honest reading is the same, because the meter starts when processing starts and cancelling after that recovers nothing.

Building the queue yourself moves the problem rather than solving it. Now you hold jobs in your own queue and you have to decide, at the exact instant a request to cancel arrives, whether a worker has already picked the job up. Get that race wrong in one direction and you refund a job that ran; get it wrong in the other and you charge for one that never did.

## The code

`cancel` is free and takes the handle a queued job returned, the same one you would pass to `result`.

```
listen(key="<key>")
  -> handle, status: running, estimated_cost_eur, estimated_minutes

cancel(handle="<handle>")
  -> cancelled: true, refunded_eur

# once an engine has started, the same call answers honestly:
#   -> cancelled: false, refunded_eur: 0, reason
#   and the job runs to the end, so you still collect it:

result(handle="<handle>")
  -> the transcript, as usual
```

## What it costs

`cancel` costs nothing. It is one of the free calls, along with upload, result, account, and calling it on a job you turn out to be too late for is free as well.

A cancelled job returns the credit that was set aside for it. `refunded_eur` is the amount that came back, and it is a real release of reserved credit, not a coupon and not a gesture.

That reservation is also why the boundary is where it is. Credit is committed before the work starts and settled when it finishes, and the charge never exceeds what was committed. While the job waits, the commitment can be undone. After an engine has read the file, the money is out, and no answer we could give would put it back.

## What this does not do

- It works only while the job is still waiting. `cancelled` comes back false when it is too late, and `reason` says why: that is the whole contract, and it is not softened anywhere.
- Work that ran inline never had a queue to sit in. There is nothing to cancel, only a result you already have.
- Cancelling is not deleting. It stops a job that has not started; it does not remove a finished result, and results expire on their own.
- Calling it twice on the same handle is safe and changes nothing. The second call is not an error and does not refund twice.
