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

# Virtual numbers

> Give every end-user their own Saudi number that answers with their instructions.

A **virtual number** is a Saudi DID assigned to one of *your* end-users. When
someone dials it, Wave answers and plays **that user's** standing instructions —
a gate code, delivery notes, a greeting — or forwards the call to their real
number without revealing it. Your user never has to publish their personal number.

<Note>
  Virtual numbers use a **production** API key (`sk_live_`) and the
  `virtual_numbers:write` / `virtual_numbers:read` scopes. Ask your Wave contact to
  enable the feature for your account.
</Note>

## Allocate a number to a user

`POST /v1/virtual-numbers`. Identify the end-user with **your own** id
(`external_user_id`) — the call is **idempotent** on it, so a retry returns the
same number rather than allocating a second one.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.wave.sa/v1/virtual-numbers \
    -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "external_user_id": "user_8842",
      "display_name": "Layla",
      "instructions": {
        "text": "Please leave the parcel at the door. Gate code 4471.",
        "language": "ar",
        "forward_to": "+966541704013"
      }
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch("https://api.wave.sa/v1/virtual-numbers", {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_live_xxxxxxxxxxxx",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      external_user_id: "user_8842",
      display_name: "Layla",
      instructions: {
        text: "Please leave the parcel at the door. Gate code 4471.",
        language: "ar",
        forward_to: "+966541704013",
      },
    }),
  });
  const { data } = await res.json();
  ```
</CodeGroup>

### Request

| Field                     | Type         | Required | Notes                                                     |
| ------------------------- | ------------ | -------- | --------------------------------------------------------- |
| `external_user_id`        | string       | **yes**  | Your id for the user (≤128 chars). The idempotency key.   |
| `display_name`            | string       | no       | ≤120 chars.                                               |
| `type`                    | string       | no       | `mobile`, `landline`, `tollfree`, `unified`, `shortcode`. |
| `prefix`                  | string       | no       | Preferred numeric prefix.                                 |
| `instructions.text`       | string       | no       | Spoken to the caller (≤1000 chars).                       |
| `instructions.audio_url`  | string (URL) | no       | Played instead of `text` when set.                        |
| `instructions.language`   | string       | no       | `ar` or `en`.                                             |
| `instructions.forward_to` | string       | no       | KSA number to bridge to, with the caller's id presented.  |

### Response — `201 Created` (or `200 OK` on an idempotent retry)

```json theme={null}
{
  "data": {
    "id": "8f2c…",
    "e164": "+966590007001",
    "status": "assigned",
    "external_user_id": "user_8842",
    "display_name": "Layla",
    "contact_id": "1a9b…",
    "instructions": {
      "text": "Please leave the parcel at the door. Gate code 4471.",
      "audio_url": null,
      "language": "ar",
      "forward_to": "+966541704013"
    },
    "assigned_at": "2026-08-19T10:00:00.000Z"
  }
}
```

<Tip>
  A recorded `audio_url` plays instantly; `text` goes through text-to-speech. Set
  both and the audio wins — so you can start with text today and upgrade to a
  recording later with no code change.
</Tip>

## Update a user's instructions

`PUT /v1/virtual-numbers/:id/instructions` — edited in place, no re-allocation.

```bash cURL theme={null}
curl -X PUT https://api.wave.sa/v1/virtual-numbers/8f2c…/instructions \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "text": "I have moved. Please call when you arrive.", "language": "en" }'
```

## List & look up

`GET /v1/virtual-numbers` — filter by `external_user_id`, page with `limit`
(≤100) and `offset`. `GET /v1/virtual-numbers/:id` returns one.

```json theme={null}
{
  "data": [ { "id": "8f2c…", "e164": "+966590007001", "external_user_id": "user_8842", "…": "…" } ],
  "meta": { "limit": 50, "offset": 0 }
}
```

## Release a number

`DELETE /v1/virtual-numbers/:id` returns the DID to the pool and frees the user to
be assigned a different one later.

```json theme={null}
{ "id": "8f2c…", "status": "released" }
```

<Warning>
  Instructions are your end-users' **personal data** — a gate code, an address, a
  real phone number. Wave never logs their content. Treat the numbers and
  instructions you store with the same care.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="How voice works" icon="phone-volume" href="/voice/overview">
    The call lifecycle behind a virtual number.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/webhooks">
    Get notified when a virtual number is dialed.
  </Card>
</CardGroup>
