> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gpuoutlet.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Rental lifecycle

> The five statuses, what each one guarantees, and how to poll without getting it wrong.

A rental moves through five statuses. Getting the transitions right is most of
what a correct integration does.

```
                  ┌──────────────┐
  POST /rentals → │ provisioning │
                  └──────┬───────┘
                    ┌────┴─────┐
                    ▼          ▼
              ┌─────────┐  ┌────────┐
              │ running │  │ failed │  ← never became usable
              └────┬────┘  └────────┘
                   │ stop, preemption, spend limit
                   ▼
              ┌──────────┐
              │ stopping │  ← still billing, still holds a slot
              └────┬─────┘
                   ▼
              ┌─────────┐
              │ stopped │  ← accrued_cents is final
              └─────────┘
```

| Status         | Usable?   | Billing                      | `access`             |
| -------------- | --------- | ---------------------------- | -------------------- |
| `provisioning` | No        | Hold placed, nothing accrued | `null`               |
| `running`      | **Yes**   | Accruing per second          | Populated            |
| `stopping`     | No        | Final charge not settled     | May still be present |
| `stopped`      | No        | `accrued_cents` is final     | `null`               |
| `failed`       | Never was | Nothing charged              | `null`               |

`provisioning` and `running` and `stopping` are all **live**: each occupies a slot
under the [concurrency ceiling](/api/spend-limits#concurrent-rentals).

## Polling

```python theme={null}
rental = create_rental(...)

while rental["status"] == "provisioning":
    time.sleep(3)
    rental = get_rental(rental["id"])

if rental["status"] == "failed":
    handle_failure(rental["failure_code"])
else:
    connect(rental["access"])
```

<Warning>
  Loop while the status **is** `provisioning`, not while it **is not** `running`.
  The second version spins forever on a `failed` rental — the most common bug in a
  first integration, and one that only shows up on a bad day.
</Warning>

Poll every two to three seconds. Provisioning takes tens of seconds; polling five
times a second spends your whole [rate limit](/api/rate-limits) to learn the same
thing.

## Two traps worth stating plainly

<AccordionGroup>
  <Accordion title="`201 Created` does not mean ready" icon="hourglass">
    `POST /rentals` returns immediately with `provisioning`. The machine does not
    exist yet, `access` is `null`, and nothing is reachable. Everything after the
    create call has to wait for `running`.
  </Accordion>

  <Accordion title="`stopping` does not mean stopped" icon="clock">
    `POST /rentals/{id}/stop` returns `stopping`. Teardown is asynchronous and the
    final charge is computed when the machine is actually gone.

    Two consequences:

    * `billing.accrued_cents` is **not final** until the status is `stopped`.
    * The rental still occupies a concurrency slot. Stop one and immediately start
      its replacement, and you can hit `max_active_rentals_reached` with what feels
      like a single machine.

    Poll until `stopped` before doing either.
  </Accordion>
</AccordionGroup>

## Why it ended

Once a rental leaves `running`, `termination_reason` says why:

| Value                 | Meaning                                                                           |
| --------------------- | --------------------------------------------------------------------------------- |
| `stopped_by_user`     | You, or someone on your account, stopped it                                       |
| `preempted`           | A spot rental was reclaimed — see [Spot](/api/rental-billing#spot-and-preemption) |
| `stopped_by_operator` | We stopped it; support will have the reason                                       |
| `spend_limit_reached` | The [daily spend cap](/api/spend-limits) on the creating key was crossed          |
| `failed`              | It ended without ever running                                                     |
| `null`                | Still live                                                                        |

## When it never came up

`status: "failed"` means the machine never became usable. **Nothing is charged**
for a failed rental. `failure_code` says what to do next:

<AccordionGroup>
  <Accordion title="provisioning_failed" icon="wrench">
    The machine was allocated but never came up. Retry, or pick another offer —
    retrying the same offer is reasonable once, since this is often a single bad host.
  </Accordion>

  <Accordion title="capacity_unavailable" icon="cloud-exclamation">
    The capacity disappeared during start-up. Retry with backoff; prefer a different
    offer if it happens twice.
  </Accordion>

  <Accordion title="internal_error" icon="bug">
    Our fault. Quote the rental id to [support](mailto:help@gpuoutlet.ai).
  </Accordion>
</AccordionGroup>

<Note>
  We do not publish the upstream's error text. It names infrastructure this API
  deliberately keeps out of view, and its wording changes without notice — so a
  handler written against it would break silently. The three codes above are stable
  and are what you should branch on.
</Note>

## The event history

Rather than diffing polled objects to work out what happened, read the history:

```bash theme={null}
curl -s https://api.gpuoutlet.ai/v1/rentals/rnt_8h2k4m/events \
  -H "Authorization: Bearer $GPUOUTLET_API_KEY"
```

```json theme={null}
{
  "data": [
    { "id": "evt_1", "type": "rental.created",  "created_at": "2026-07-29T14:02:11.000Z", "data": { } },
    { "id": "evt_2", "type": "rental.running",  "created_at": "2026-07-29T14:02:47.000Z", "data": { } },
    { "id": "evt_3", "type": "rental.stopping", "created_at": "2026-07-29T17:31:02.000Z", "data": { } },
    { "id": "evt_4", "type": "rental.stopped",  "created_at": "2026-07-29T17:31:40.000Z", "data": { } }
  ],
  "meta": { "next_cursor": null }
}
```

Append-only, **oldest first**, cursor-paginated. Events are pruned after 90 days.

| Event              | When                         |
| ------------------ | ---------------------------- |
| `rental.created`   | The rental was accepted      |
| `rental.running`   | It became usable             |
| `rental.stopping`  | Teardown was requested       |
| `rental.stopped`   | It is gone; billing is final |
| `rental.preempted` | A spot rental was reclaimed  |
| `rental.failed`    | It never became usable       |

`data` holds the rental as it looked when the event was recorded. History is not
rewritten by today's serializer, so an old event may lack fields added since —
read it defensively.

<Tip>
  These are the same event names webhook deliveries will carry. A handler written
  against this list keeps working when webhooks ship, so it is worth shaping your
  code around events now rather than around polled diffs.
</Tip>
