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

# Rentals

> Rent a number for hours, days or weeks — create, extend, swap, cancel and read its SMS.

A rental holds a number for a fixed duration instead of a single verification. Rentals are
reachable with a **normal API key** — the same `x-api-key` you use for verifications, including
one [minted through x402](/docs/guides/x402).

Base path: `https://virtualsms.io/api/v1/rentals`

## Two rental types

| `rental_type` | What you get                                   |
| ------------- | ---------------------------------------------- |
| `service`     | The number is locked to one service you choose |
| `full`        | Full access to the number for any service      |

## Catalogue (public, no key needed)

| Method | Path                        | Returns                                                          |
| ------ | --------------------------- | ---------------------------------------------------------------- |
| `GET`  | `/api/v1/rentals/available` | Countries that currently have rentals, with per-duration pricing |
| `GET`  | `/api/v1/rentals/services`  | Services available in one country for one duration               |
| `GET`  | `/api/v1/rentals/price`     | Price for one service + country + duration                       |
| `GET`  | `/api/v1/rentals/pricing`   | The full active rental price list                                |

### GET /api/v1/rentals/available

Optional query params: `country`, `service`, `type` (`service` | `full`), `provider`,
`duration_hours`.

```json theme={null}
{
  "countries": [
    {
      "country_code": "DE",
      "country_name": "Germany",
      "flag": "🇩🇪",
      "availability": "medium",
      "pricing": {
        "service": [ { "duration_hours": 24, "duration_label": "1 Day", "price": 2.50 } ],
        "full": [ { "duration_hours": 168, "duration_label": "7 Days", "price": 19.99 } ]
      }
    }
  ],
  "total_availability": "high",
  "full_access_countries": [
    {
      "country_code": "DE",
      "country_name": "Germany",
      "flag": "🇩🇪",
      "availability": "medium",
      "pricing": { "24": 9.99, "168": 49.99 }
    }
  ]
}
```

<Note>
  Callers without an API key get banded availability (`availability`, `total_availability`).
  Callers presenting a valid API key get exact counts in `available_count` and `total_available`
  instead. Pass your key if you need to size an order programmatically.
</Note>

### GET /api/v1/rentals/services

`country_code` (ISO alpha-2) or `country_id` is required; `duration` defaults to `24`.

```bash theme={null}
curl "https://virtualsms.io/api/v1/rentals/services?country_code=DE&duration=24"
```

Returns an array of `{ service_id, service_name, physical_count, our_price, popular, icon_url }`,
where `physical_count` is an availability indicator for that service.

### GET /api/v1/rentals/price

`service` and `duration` (integer hours) are required, plus `country_code` (preferred) or the
legacy numeric `country`.

```json theme={null}
{ "price": 4.50, "duration_hours": 24 }
```

### GET /api/v1/rentals/pricing

Takes no parameters. Returns the active price list as an array of
`{ rental_type, duration_hours, duration_label, base_price, country_code, service_id }`.

## Create a rental

<ParamField path="POST /api/v1/rentals" type="authenticated" />

| Body field       | Type    | Required                            |
| ---------------- | ------- | ----------------------------------- |
| `country`        | string  | yes                                 |
| `rental_type`    | string  | yes — `service` or `full`           |
| `duration_hours` | integer | yes — must be greater than 0        |
| `service`        | string  | yes when `rental_type` is `service` |
| `auto_renew`     | boolean | no                                  |

```bash theme={null}
curl -X POST https://virtualsms.io/api/v1/rentals \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"country": "DE", "rental_type": "service", "service": "wa", "duration_hours": 24}'
```

```json theme={null}
{
  "success": true,
  "rental_id": "c1b7e0a2-9d34-4f68-8b15-2a7c6e40f931",
  "phone_number": "+491701234567",
  "rental_type": "service",
  "service": "wa",
  "duration": "24h",
  "price": 4.50,
  "started_at": "2026-09-07T12:00:00Z",
  "expires_at": "2026-09-08T12:00:00Z",
  "auto_renew": false,
  "status": "active",
  "provider": "local"
}
```

`provider` is either `local` or `network` — which pool the number came from. Nothing more
specific is ever returned.

### Errors

| Status | Meaning                                                                                                                  |
| ------ | ------------------------------------------------------------------------------------------------------------------------ |
| `400`  | Missing `country` / `rental_type` / `duration_hours`, missing `service` on a service rental, or an unknown `rental_type` |
| `402`  | Insufficient balance — body carries `required` and `current`                                                             |
| `404`  | No available numbers for rental in this country                                                                          |
| `409`  | The requested configuration is unavailable                                                                               |

## List your rentals

<ParamField path="GET /api/v1/rentals" type="authenticated" />

Optional `?status=` — defaults to `active`, pass `all` for every status. Returns an array of
`{ id, phone_number, rental_type, service_id, duration_hours, started_at, expires_at, price,
auto_renew, status, sms_received, sms_forwarded, last_sms_at, provider }`.

## Read the SMS

<ParamField path="GET /api/v1/rentals/{id}/sms" type="authenticated" />

```json theme={null}
{
  "rental_id": "c1b7e0a2-9d34-4f68-8b15-2a7c6e40f931",
  "phone_number": "+491701234567",
  "status": "active",
  "messages": [
    {
      "id": "5c0e1f8a-31b7-42d0-9e6a-8f4b0c72a115",
      "sender": "WhatsApp",
      "message": "Your code is 123456",
      "service_detected": "wa",
      "received_at": "2026-09-07T12:05:00Z",
      "delivery_method": "sms",
      "event_type": "sms"
    }
  ],
  "count": 1
}
```

Voice events appear in the same list with `event_type: "voice"` and additional `status`,
`extracted_code`, `duration_sec` and `caller_id` fields. Newest first, capped at 100 messages.

## Cancel — full refund inside the window

<ParamField path="POST /api/v1/rentals/{id}/cancel" type="authenticated" />

No request body.

```json theme={null}
{ "success": true, "rental_id": "c1b7e0a2-...", "refund": 4.50, "status": "cancelled" }
```

Cancellation is allowed while **both** of these hold:

* **within 20 minutes** of the rental starting, and
* **no SMS has arrived yet** on the number.

The refund covers the rental price **plus any extension you bought inside the same window**.

<Warning>
  A `network` service rental additionally cannot be cancelled in its **first 2 minutes** — the
  call returns `400` until the rental is two minutes old. So the practical cancel window for those
  is roughly minute 2 to minute 20.
</Warning>

| Status        | Meaning                                                                            |
| ------------- | ---------------------------------------------------------------------------------- |
| `400`         | Outside the 20-minute window, inside the 2-minute floor, or an SMS already arrived |
| `404`         | Rental not found or already cancelled                                              |
| `502` / `503` | Cancellation temporarily unavailable — retry shortly                               |

## Swap — a new number, same rental

<ParamField path="POST /api/v1/rentals/{id}/swap" type="authenticated" />

No request body. Replaces the number without ending the rental; `expires_at` is unchanged.

```json theme={null}
{
  "success": true,
  "rental_id": "c1b7e0a2-...",
  "phone_number": "+491709999999",
  "expires_at": "2026-09-08T12:00:00Z"
}
```

Eligibility is narrow — all of these must hold:

* `rental_type` is `service` and `provider` is `network`
* no SMS has arrived yet
* the rental is **between 2:00 and 19:30 old**

| Status | Meaning                                                                                                                                                                 |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `409`  | Not eligible, or outside the 2:00–19:30 window                                                                                                                          |
| `503`  | No replacement number available (`"code": "swap_no_stock"`) or swap temporarily unavailable                                                                             |
| `502`  | The new number is live but releasing the old one is still retrying — `"code": "swap_release_pending"`, and `swapped` is `true`. Your new number works; no action needed |

## Extend

<ParamField path="POST /api/v1/rentals/{id}/extend" type="authenticated" />

Body: `{"duration_hours": 24}` — required and non-zero.

```json theme={null}
{
  "success": true,
  "rental_id": "c1b7e0a2-...",
  "new_expires_at": "2026-09-09T12:00:00Z",
  "price": 2.00
}
```

`network` rentals accept only **24, 72 or 168** hours; anything else returns `400`. A `402`
carries `required` and `current`. If the extension fails upstream you get `502` and are not
charged.

## Release early — partial refund, local rentals only

<ParamField path="POST /api/v1/rentals/{id}/release" type="authenticated" />

```json theme={null}
{
  "success": true,
  "rental_id": "c1b7e0a2-...",
  "status": "completed",
  "hours_used": "3.25",
  "refund": 2.10,
  "message": "Rental released successfully. Refund added to store credit."
}
```

* Only rentals with `provider: "local"` can be released early. A `network` rental returns `400`
  and runs to its natural expiry — past the 20-minute cancel window there is no early exit for it.
* The rental must be at least **2 hours** old.
* The refund is pro-rata: the unused portion of the price, minus a **10% fee**. For a `full`
  rental, any services already used are deducted instead of elapsed time when that is larger.
  The result is never negative and never exceeds what you paid.

## Report a problem

<ParamField path="POST /api/v1/rentals/{id}/report" type="authenticated" />

| Body field    | Required             | Notes                                                  |
| ------------- | -------------------- | ------------------------------------------------------ |
| `service_id`  | on `service` rentals | Must match the rental's service                        |
| `issue_type`  | no                   | `no_sms` (default), `wrong_service`, `banned`, `other` |
| `description` | no                   | Free text                                              |

```json theme={null}
{
  "success": true,
  "issue_id": "0a3f7c15-6b28-4e9d-91f0-3c85b4d27e60",
  "manual_review": true,
  "message": "Report received. Our team will review and follow up."
}
```

Reporting queues a human review — it does not issue an automatic refund or replacement.
Reporting the same rental twice returns `409` with the existing `issue_id`.
