# Agent quickstart

An agent can go from nothing to a running sandbox on its own: one call for an account and its key, one payment, one call for the sandbox. This page walks that path over REST.

`POST https://api.rent.dev/v1/accounts`

## Create an account

`POST /v1/accounts` takes no credentials and no body. It answers 201 with the account and its first API key. The key's `secret` is shown this once, so store it.

```sh
curl -sX POST https://api.rent.dev/v1/accounts
```

```json
{
  "account_id": "acc_01JCZ9Q4M8V7K2T3N5P6R8S0W1",
  "account": {
    "id": "acc_01JCZ9Q4M8V7K2T3N5P6R8S0W1",
    "created_at": "2026-11-10T22:00:00.000Z",
    "frozen": false,
    "recovery": {
      "agent_key": false,
      "wallet": false,
      "passkey": false
    }
  },
  "api_key": {
    "id": "key_01JCZ9Q4M8V7K2T3N5P6R8S0W1",
    "prefix": "rent_sk_0f0f",
    "name": null,
    "created_at": "2026-11-10T22:00:00.000Z",
    "last_used_at": null,
    "revoked_at": null,
    "spend_limit_micros": null,
    "spent_micros": 0,
    "secret": "rent_sk_0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"
  }
}
```

The examples below read the secret from `RENT_KEY`.

To be able to repeat the signup, send an `Idempotency-Key` header with a random value you keep. Repeating the call with it within 24 hours answers the same account with a new key that replaces the first, while the account is untouched: its key never used, nothing bound to it and no money in. After that the repeat answers 422 `idempotency_key_reused` and no key. Treat the value as a secret for those 24 hours.

```sh
curl -sX POST https://api.rent.dev/v1/accounts -H "Idempotency-Key: $SIGNUP_KEY"
```

## Authenticate

Send the key as `Authorization: Bearer rent_sk_…` or as `X-API-Key`. The same key written `e2b_` plus the same 64 hex characters is accepted everywhere, which is how the E2B SDK uses it.

An API key carries the scopes `sandbox`, `gpu`, `billing` and `keys`. An OAuth access token from `https://auth.rent.dev` carries the scopes it was granted.

A key can carry a spend limit, `spend_limit_micros`. The limit counts what rent.dev bills from the prepaid balance: its own sandboxes, with what their holds reserve. GPU pods and sandboxes that run on your own vendor key are billed by that vendor and do not count against it. A credential without the `gpu` scope cannot start GPU pods, but every API key carries `gpu`, so a key with a limit can still start a pod on a RunPod key the account has stored.

Such a key is a budget someone handed out, not authority over the account. These calls answer 403 `forbidden` for it:

- Keys: it cannot create, update, rotate or revoke other keys. It can list them, and rotate or revoke itself.
- Agent keys, wallets and people: it cannot bind or remove agent keys, or remove wallets or people. It can list them.
- How the account pays: it cannot set auto-recharge or start saving a payment method.
- Vendor keys: it cannot store or delete them.
- Sandboxes another key started: it cannot extend or resume them.

It still reads the balance, the ledger and the auto-recharge settings, creates a top-up or a pay link, tops up over x402, and extends or resumes the sandboxes it started. The key that replaces it keeps only what is left of the limit after what the old key spent and still holds.

## Bind a key the agent holds

A lost API key cannot be read again. To be able to get a new one, bind an Ed25519 key the agent holds. Get a nonce (single use, valid 5 min), sign the text `rent.dev signup v1\n{nonce}\n{public_key}`, and send the public key, the nonce and the signature.

| Call | What it does |
| --- | --- |
| `GET /v1/accounts/nonce` | Get a single-use nonce for signing up, binding or recovering with an agent key |
| `POST /v1/accounts` | With `public_key`, `nonce` and `signature` in the body: signs up and binds the key in one call. |
| `POST /v1/accounts/agent-key` | Bind an Ed25519 agent key to this account |
| `POST /v1/accounts/recover` | A new API key for a signature over `rent.dev recover v1\n{nonce}\n{public_key}`. |

Keys, nonces and signatures are base64url without padding: 43 characters for a public key or a nonce, 86 for a signature.

## Fund the balance

A new account has a zero balance. A call that needs credit answers 402 with a Problem that says how much is missing and carries two ways to pay it. This one is what creating the default sandbox answers on an empty balance:

```json
{
  "type": "https://rent.dev/problems/insufficient_balance",
  "title": "Insufficient balance",
  "status": 402,
  "code": "insufficient_balance",
  "docs_url": "https://rent.dev/docs/errors#insufficient_balance",
  "request_id": "req_01JCZ9Q4M8V7K2T3N5P6R8S0W1",
  "balance_required_micros": 9075,
  "topup": {
    "checkout_url": "https://api.rent.dev/pay/<token>",
    "x402_url": "https://api.rent.dev/v1/credits/x402?amount_usd=0.009075"
  }
}
```

- `topup.x402_url`: pay it yourself in USDC over x402.
- `topup.checkout_url`: a pay link for a person. It opens Stripe Checkout, needs no key and works for 24 hours.

To add credit ahead of time, call the x402 route with the amount you want or create a Checkout Session. [Payments](/docs/payments) covers both.

```sh
# USDC over x402: answers 402 with the payment requirement, then 200 once paid
curl -sX POST "https://api.rent.dev/v1/credits/x402?amount_usd=5" -H "Authorization: Bearer $RENT_KEY"

# or a Checkout page for a person: the answer carries checkout_url
curl -sX POST https://api.rent.dev/v1/credits/checkout -H "Authorization: Bearer $RENT_KEY" -H "Content-Type: application/json" -d '{"amount_usd":"5.00"}'
```

## Create a sandbox

`POST https://api.rent.dev/v1/sandboxes`

With an empty body you get the `base` template and a 300 second timeout. rent.dev first holds the most the sandbox could cost for that timeout ($0.009075 for `base`), then places it.

```sh
curl -sX POST https://api.rent.dev/v1/sandboxes -H "Authorization: Bearer $RENT_KEY" -H "Prefer: wait=30" -H "Content-Type: application/json" -d '{}'
```

With `Prefer: wait=30` the answer is 201 and the sandbox when it is ready within 30 seconds. Otherwise it is 202 and an Operation: poll `GET /v1/operations/{id}`, waiting `Retry-After` seconds between polls, until `done` is true. Then `response` holds the sandbox, or `error` holds a Problem.

| Body field | Type | Description |
| --- | --- | --- |
| `template` | `string` | Template alias: the caller's own or the public set. Default `base`. |
| `timeout_s` | `integer, 1 to 86400` | Seconds until the sandbox stops (or pauses with auto_pause). Default 300. |
| `metadata` | `Metadata` | Caller metadata. Keys starting `rentdev_` are reserved and dropped. |
| `env_vars` | `map of string to string` |  |
| `auto_pause` | `boolean` | Pause instead of stopping at the timeout. Default false. |
| `auto_resume` | `boolean` | Resume a paused sandbox when traffic arrives. Default false. |
| `allow_internet_access` | `boolean` |  |
| `network` | `SandboxNetwork` |  |
| `profile` | `agent-loop \| interactive \| workspace` | Expected duty, used for placement when no history exists. |
| `routing` | `SandboxRouting` |  |

The current node has two public templates: `base` (2 vCPU, 512 MiB) and `code-interpreter-v1` (2 vCPU, 2 GiB), which adds a Jupyter kernel for `run-code`.

> Whether own sandboxes can be placed right now is on [the status page](/status) and at `GET /v1/status`. When no endpoint can take a request the answer is 503 `no_capacity`.

## Run commands and move files

```sh
curl -sX POST https://api.rent.dev/v1/sandboxes/$SANDBOX_ID/exec -H "Authorization: Bearer $RENT_KEY" -H "Content-Type: application/json" -d '{"cmd":"echo hello"}'
```

`exec` waits for the command and answers its stdout, stderr and exit code. The command is killed after `timeout_s` seconds (default 50). For anything longer, start a process and read its output with a cursor.

| Call | What it does |
| --- | --- |
| `POST /v1/sandboxes/{id}/exec` | Run a command and wait for it |
| `POST /v1/sandboxes/{id}/processes` | Start a background process |
| `GET /v1/sandboxes/{id}/processes/{process_id}/output` | Read a process's output after a cursor |
| `POST /v1/sandboxes/{id}/processes/{process_id}/stdin` | Write to a process's stdin |
| `DELETE /v1/sandboxes/{id}/processes/{process_id}` | Kill a process |
| `GET /v1/sandboxes/{id}/files` | List a directory |
| `GET /v1/sandboxes/{id}/files/content` | Read a file, streamed |
| `PUT /v1/sandboxes/{id}/files/content` | Write a file, streamed |
| `POST /v1/sandboxes/{id}/run-code` | Run code on the code-interpreter-v1 kernel |
| `GET /v1/sandboxes/{id}/ports/{port}/url` | Public URL of a port inside the sandbox |
| `GET /v1/sandboxes/{id}/logs` | Sandbox logs as server-sent events |

## Stop and settle

`DELETE /v1/sandboxes/{id}` stops the sandbox. The last usage is charged and the rest of the hold returns to the available balance. A sandbox also stops at its timeout; `POST /v1/sandboxes/{id}:extend` moves the timeout, up to 24 h from now.

`:pause` keeps memory and disk and stops the meter. `:resume` takes a new hold and continues where the sandbox left off.

## Retry safely

Send an `Idempotency-Key` header on any `POST` under `/v1`. The first 2xx answer is replayed for 24 hours with `Idempotent-Replayed: true`. The same key with a different request answers 422, and while the first request is still running it answers 409. A refusal is not stored, so after a 402 you can top up and send the same key again.

Keys are tracked per account. Signup has no account yet and keeps a rule of its own: see [Create an account](#account).

## Tokens without an API key

An agent that has bound an Ed25519 key can trade a signed assertion for a one-hour access token, with no API key in the request:

```sh
curl -sX POST https://auth.rent.dev/oauth/token \
  -d grant_type=client_credentials \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion=$ASSERTION \
  -d resource=https://api.rent.dev
```

| Claim | Value |
| --- | --- |
| `alg` (header) | `EdDSA` |
| `typ` (header) | `client-authentication+jwt`. An assertion without it is still accepted. |
| `iss` and `sub` | The account id, `acc_…`. |
| `aud` | `https://auth.rent.dev`, the issuer, as the only value. The token endpoint's URL is refused. |
| `exp` | At most 5 minutes ahead. |
| `jti` | Unique. Each value is accepted once. |

`resource` names the one API the token is for: `https://api.rent.dev` or `https://mcp.rent.dev/mcp`. Without `scope` the token carries what an API key carries. No refresh token is issued; sign a new assertion instead.

The device flow (RFC 8628) serves one client, `rentdev-cli`: it posts to `https://auth.rent.dev/device_authorization`, and a person enters the code at `https://auth.rent.dev/device`. A code lasts 10 minutes and the client polls every 5 seconds.

People sign in with a passkey at `https://auth.rent.dev`. Other OAuth clients use the authorization code flow with PKCE; the server's metadata is at `https://auth.rent.dev/.well-known/oauth-authorization-server`.

## Other ways in

- [MCP](/docs/mcp): the same account, sandboxes and payments as tools.
- [E2B SDK](/docs/e2b): the unmodified SDK, pointed at rent.dev.
- [REST API](/docs/rest): every route, parameter and status.
- [Pricing](/docs/pricing) and [limits](/docs/limits).
