---
title: "Create a long-term rental"
method: POST
path: "/rentals"
tags: ["Rentals"]
---

# Create a long-term rental

`POST /rentals`

## Request

- Base URL: `https://dashboard.voidmob.com/api/v1`
- URL: `https://dashboard.voidmob.com/api/v1/rentals`
- Auth: HTTP bearer

Reserves a dedicated phone number for a fixed duration (3, 7, 14, or 30
days). All incoming SMS messages are collected and accessible via
`GET /v1/rentals/{id}`. Idempotent by `Idempotency-Key`.

## Price over cap

LTR pricing can drift. If the lowest available price exceeds your
`max_price_cents` ceiling at request time, the API returns
**`409 PRICE_OVER_CAP`** with both values in the error details:

```json
{
  "success": false,
  "error": {
    "code": "PRICE_OVER_CAP",
    "details": {
      "max_price_cents": 1500,
      "available_price_cents": 1750
    }
  }
}
```

No partial state is created and no funds are touched. Decide locally
whether to retry with a higher cap or surface the price to the user.

## Duration

`duration` must be one of `3D`, `7D`, `14D`, or `30D`. Only
`country="us"` is currently supported.

## Auto-renew

Set `auto_renew: true` to automatically charge the same duration
shortly before `paid_until`. Toggle at any time via
`POST /v1/rentals/{id}/auto_renew`. If the balance cannot cover the
renewal in time, auto-renew is turned off and
`rental.auto_renew_disabled` fires.

## Headers

- `Idempotency-Key` string, required

## Request body

- RentalCreateRequest
  - `service_id` string, required: Opaque public service identifier.
  - `duration` '3D' | '7D' | '14D' | '30D', required: Rental period. Must be one of `3D`/`7D`/`14D`/`30D`.
  - `country` string: ISO-3166-1 alpha-2, lowercased. Only `us` is supported.
  - `auto_renew` boolean: Automatically renew the rental for the same duration at expiry.
  - `max_price_cents` integer: Optional price ceiling. Drift within the cap charges silently; drift above the cap returns `409 PRICE_OVER_CAP` with the current available price in the error details so callers can decide whether to retry with a higher cap. Omit to require an exact match to the current catalog price.
  - `webhook_endpoint_id` string: Webhook endpoint identifier.

## Response `201`

Rental created. `status` is `active` immediately.

- object
  - `success` true, required
  - `data` object, required
    - `id` string, required: Opaque long-term rental identifier.
    - `display_id` string, nullable: Human-readable order identifier for display in dashboards and receipts (e.g. `SMS7K4P2N`). Null for legacy rows.
    - `status` 'active' | 'expired' | 'cancelled', required
    - `phone_number` string, required: E.164 phone number (with leading +).
    - `service_id` string, required: Opaque public service identifier.
    - `service_name` string, required: Human-readable service name.
    - `country` string, required: ISO-3166-1 alpha-2, lowercased.
    - `duration` '3D' | '7D' | '14D' | '30D', required: Rental period.
    - `rental_type` 'rental', required: Always `rental` for long-term rentals.
    - `charged_price_cents` integer, required: Actual amount deducted from the wallet.
    - `pricing_source` 'regular' | 'category' | 'override' | 'promo': Attribution for the charged price. Omitted for legacy rows. "promo" means a redeemed service-discount code priced this purchase.
    - `auto_renew` boolean, required: Whether the rental will automatically renew at expiry.
    - `next_renewal_price_cents` integer, required: Estimated cost of the next auto-renewal at current pricing.
    - `payment_due` PaymentDue, required: A renewal that landed before it could be paid. The term is renewed but owed; the amount is collected automatically as soon as the balance covers it. SMS received meanwhile are withheld (`content_withheld`) until it is paid, and stay withheld if the term ends unpaid.
      - `amount_cents` integer, required: Amount owed, in USD cents.
      - `since` string, date-time, required: When the renewal landed unpaid.
    - `re_rent_available` boolean, required: Whether this rental is eligible for re-rent on the same number after it expires. `true` only when the rental is `expired` and the number has not yet been released. `false` for all non-expired rentals and for expired rentals that are no longer eligible.
    - `re_rent_price_cents` integer, nullable, required: Current re-rent price in USD cents. Non-null only when `re_rent_available` is `true`. Null for all other states.
    - `re_rent_blocked_at` string, date-time, nullable, required: ISO 8601 timestamp at which the number was permanently released. Non-null means re-rent will never become available for this rental. Null when still eligible (or for non-expired rentals).
    - `created_at` string, date-time, required
    - `paid_until` string, date-time, required: Timestamp through which the number is paid.
    - `expires_at` string, date-time, required: Timestamp at which the rental expires (same as `paid_until` for standard rentals).
    - `can_cancel` boolean, required: Whether the rental is currently cancellable. True only within the first hour after creation and only for non-dedicated rentals that have not been flagged.
    - `cancel_window_expires_at` string, date-time, nullable: Timestamp at which the cancel window closes. Null for dedicated 28-day numbers (which cannot be cancelled).
    - `messages` RentalMessage[], required: SMS messages received on this number, sorted oldest-first. A message received while payment is due has `text` and `code` null and `content_withheld: true`.
      - `id` string, required: Opaque message identifier (prefixed `msg_`).
      - `code` string, nullable: Extracted numeric code, if any. Null when the SMS is informational or its content is withheld.
      - `text` string, nullable, required: Full raw SMS text. Null only while the content is withheld.
      - `content_withheld` true: Present (always `true`) only on an SMS received while a renewal is unpaid (`payment_due`): stored, but `text` and `code` are null until the due is paid. Absent on every other message.
      - `received_at` string, date-time, required

## Other responses

- `400`: 400 - Request failed validation.
- `401`: 401 - Authentication required.
- `402`: 402 - Your wallet balance is too low for this purchase.
- `403`: 403 - Your account cannot perform this action.
- `404`: Not found. Body discriminates by `error.code`: - `SERVICE_NOT_FOUND` - unknown service id - `LTR_NOT_AVAILABLE` - service does not offer the requested duration - `WEBHOOK_ENDPOINT_NOT_FOUND` - `webhook_endpoint_id` is unknown or belongs to another user
- `409`: Conflicts. Body discriminates by `error.code`: - `IDEMPOTENCY_CONFLICT` - same key, different body - `IDEMPOTENCY_REPLAY_IN_FLIGHT` - duplicate while original processes - `PRICE_OVER_CAP` - lowest available price exceeds `max_price_cents`; see `PriceOverCapError`
- `429`: 429 - Rate limit exceeded. Retry after the indicated interval.
- `500`: 500 - An unexpected error occurred. Please try again.
- `502`: 502 - Service error. Please retry shortly.
- `503`: 503 - This service is temporarily unavailable. Please try again shortly.
- `504`: 504 - Request timed out. Funds have been refunded.

---

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