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

# Worked SIP credential samples

> Registering agent handsets: page the credentials, issue one with outbound + recording settings, rotate its password, force-unregister a device — plus the 403 and 409 branches clients must code.

## Worked SIP credential samples

SIP credentials are usually the first calls a contact-center integration makes — they register each agent's softphone or desk phone. Copy a request as written, substitute your own ids, and compare the response envelope. Successful writes return `{ data, meta }`; failures return the `{ error, meta }` envelope. The retry-vs-surface matrix for every class sits at the bottom of this page.

A credential owns a softphone registration on the inbound SIP edge: the device REGISTERs against `sip.orbit.devotel.io`, inbound calls ring it, and outbound usage (if enabled) is tracked against the credential. The samples below pair cURL with Node — reads use a raw `fetch`; the SIP-credential surface has no typed Node SDK resource yet, so the write tabs use the `orbit.request` escape hatch pattern from the [API recipes](/guides/api-recipes).

### Page the credentials

<Note>
  `GET /api/v1/sip-credentials`
</Note>

Lists non-deleted credentials newest-first with keyset pagination. `limit` defaults to 25 and caps at 200; a value above the cap is silently clamped, not rejected. When `meta.pagination.has_more` is true, pass `meta.pagination.cursor` back as the `cursor` query parameter for the next page.

**Request**

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/sip-credentials?limit=25" \
      -H "X-API-Key: $ORBIT_API_KEY"
    ```

    ```typescript Node.js theme={null}
    const res = await fetch("https://api.orbit.devotel.io/api/v1/sip-credentials?limit=25", {
      headers: { "X-API-Key": process.env.ORBIT_API_KEY! },
    });
    console.log(await res.json());
    ```
  </CodeGroup>
</RequestExample>

**Response**

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": [
      {
        "id": "sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3",
        "username": "ac1b2c3d-front-desk",
        "label": "front-desk",
        "enabled": true,
        "extension_number": "101",
        "outbound_enabled": true,
        "registered": true,
        "sip_server": "sip.orbit.devotel.io",
        "sip_realm": "orbit.devotel.io",
        "created_at": "2026-09-10T09:41:12.884Z"
      }
    ],
    "meta": {
      "request_id": "req_sipcred_list_01",
      "timestamp": "2026-09-10T09:43:02.411Z",
      "pagination": {
        "cursor": "2026-09-10T09:41:12.884Z|sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3",
        "has_more": true
      }
    }
  }
  ```
</ResponseExample>

Every row carries the connection details a handset needs (`sip_server`, `sip_realm`, ports) so a per-row read is not required to configure a device. The plaintext password is never in a list row.

### Create a SIP credential

<Note>
  `POST /api/v1/sip-credentials`
</Note>

Issues a credential and returns the plaintext password exactly once — save it immediately; it is not recoverable. `label` is the only required field; it is lowercased and slugified into the SIP username, which stays stable across password rotations. This example wires a front-desk handset: an extension, a fixed caller ID, recording-on, and an IP allow-list.

**Request**

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/sip-credentials" \
      -H "X-API-Key: $ORBIT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "label": "front-desk",
        "extensionNumber": "101",
        "outboundEnabled": true,
        "outboundCallerIdMode": "fixed",
        "defaultCallerIdE164": "+14155550123",
        "concurrentCallCap": 2,
        "allowedIpCidrs": ["203.0.113.0/24"],
        "forceRecordOutbound": true,
        "notes": "Lobby desk phone, Yealink T54W"
      }'
    ```

    ```typescript Node.js theme={null}
    // SIP credentials have no typed helper in the Node SDK yet — use the
    // orbit.request escape hatch (same pattern as guides/api-recipes).
    import { Orbit } from "@devotel-orbit/node";

    const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });

    const created = await orbit.request("POST", "/api/v1/sip-credentials", {
      body: {
        label: "front-desk",
        extensionNumber: "101",
        outboundEnabled: true,
        outboundCallerIdMode: "fixed",
        defaultCallerIdE164: "+14155550123",
        concurrentCallCap: 2,
        allowedIpCidrs: ["203.0.113.0/24"],
        forceRecordOutbound: true,
        notes: "Lobby desk phone, Yealink T54W",
      },
    });
    console.log(created);
    ```
  </CodeGroup>
</RequestExample>

**Response**

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3",
      "username": "ac1b2c3d-front-desk",
      "label": "front-desk",
      "enabled": true,
      "extension_number": "101",
      "outbound_enabled": true,
      "password": "Kq7mD2xW9fTz1LpR4vN8sYe",
      "password_visible_once_warning": "Save this password now — it is not recoverable. Rotate via POST /sip-credentials/:id/rotate to issue a new one.",
      "sip_server": "sip.orbit.devotel.io",
      "sip_port_tls": 5061,
      "sip_port_tcp": 5060,
      "sip_realm": "orbit.devotel.io",
      "sip_transport_recommended": "TLS",
      "sip_srtp": "Mandatory",
      "created_at": "2026-09-10T09:41:12.884Z"
    },
    "meta": {
      "request_id": "req_sipcred_create_01",
      "timestamp": "2026-09-10T09:41:12.901Z"
    }
  }
  ```
</ResponseExample>

Point the handset at `sip_server` + `username` + the one-time `password`; the create response is the only place the password appears. `defaultCallerIdE164` is ownership-checked — a number your organization does not own is rejected with a 403 (folded into the matrix below, since "pick a number you own" is obvious from the message).

### Rotate a password

<Note>
  `POST /api/v1/sip-credentials/{id}/rotate`
</Note>

Generates a new password and invalidates the old one instantly. The username does not change, so registered devices keep working after you update the password field. The new plaintext password is returned exactly once, same rule as create.

**Request**

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/sip-credentials/sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3/rotate" \
      -H "X-API-Key: $ORBIT_API_KEY"
    ```

    ```typescript Node.js theme={null}
    const rotated = await orbit.request(
      "POST",
      "/api/v1/sip-credentials/sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3/rotate",
    );
    console.log(rotated);
    ```
  </CodeGroup>
</RequestExample>

**Response**

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3",
      "username": "ac1b2c3d-front-desk",
      "password": "V3xF9qR2wP8tU7eL6cN4aDs",
      "password_visible_once_warning": "Save this password now — it is not recoverable."
    },
    "meta": {
      "request_id": "req_sipcred_rotate_01",
      "timestamp": "2026-09-10T09:47:29.540Z"
    }
  }
  ```
</ResponseExample>

### Force-unregister a device

<Note>
  `POST /api/v1/sip-credentials/{id}/unregister`
</Note>

Revokes the live registration binding — the handset drops off the network immediately, and will re-register on its next re-REGISTER (default \~60s) unless you also disable the credential with a PATCH `enabled: false`. Use this for a stolen phone or a sticky NAT binding.

**Request**

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/sip-credentials/sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3/unregister" \
      -H "X-API-Key: $ORBIT_API_KEY"
    ```

    ```typescript Node.js theme={null}
    const evicted = await orbit.request(
      "POST",
      "/api/v1/sip-credentials/sipcred_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3/unregister",
    );
    console.log(evicted);
    ```
  </CodeGroup>
</RequestExample>

**Response**

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "ok": true,
      "bindings_evicted": 1
    },
    "meta": {
      "request_id": "req_sipcred_unregister_01",
      "timestamp": "2026-09-10T09:47:31.210Z"
    }
  }
  ```
</ResponseExample>

## Errors worth branching on

These failures cover the create, rotate, unregister, and read calls above plus the update (PATCH) and delete calls on the same page. Each block is a full `{ error, meta }` envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same classes. For the platform-wide decision table these branches plug into, see the [error handling guide](/guides/error-handling-examples).

### 403 — scope or caller-id ownership

```json 403 theme={null}
{
  "error": {
    "code": "FORBIDDEN",
    "message": "The key is valid but does not have the scope this operation requires.",
    "status": 403
  }
}
```

A `403` on create or rotate means the key lacks the write scope — developer and viewer roles can read credentials, and admin or owner is required to create or modify, so branch on the role and surface instead of retrying. The same 403 shape also fires when `defaultCallerIdE164` names a number your organization does not own — the call is rejected before the credential is created.

### 409 — duplicate label or extension

```json 409 theme={null}
{
  "error": {
    "code": "CONFLICT",
    "message": "A SIP credential with label \"front-desk\" or extension \"101\" already exists. Pick a different one or rotate the existing one.",
    "status": 409
  }
}
```

Recreating the same label, or reusing an extension number already taken in your organization, returns this `409 CONFLICT` — pick a new label or rotate the existing credential instead of blind-retrying. This is the same branch a teams-client codes on duplicate name: the client decides whether to label the device differently or PATCH the existing row.

### 404 — stale or foreign id

```json 404 theme={null}
{
  "error": {
    "code": "NOT_FOUND",
    "message": "SIP credential not found",
    "status": 404
  }
}
```

A `404` on get/update/rotate/unregister/delete means the `sipcred_*` id does not exist in your organization (or was soft-deleted) — re-list credentials and branch on the current id rather than retrying the stale one.

### 60-second retry matrix

| Class                                          | Meaning                                                              | Branch response                                                                                                       |
| ---------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **401 `UNAUTHORIZED`**                         | the bearer key itself fails                                          | **Surface.** Rotate the key; the resource is not wrong.                                                               |
| **403 `FORBIDDEN`**                            | scope on a write, or caller-id ownership on create                   | **Surface.** Fix the key's scope set; on ownership, pick a number from `/numbers` — a role check tells you which one. |
| **422 `VALIDATION_ERROR`**                     | a field fails the schema (e.g. `extensionNumber` outside 2-8 digits) | **Fix, then resend.** Branch on `error.details.issues[0].field`; a blind retry repeats the same 422.                  |
| **429 `RATE_LIMITED`**                         | the create/rotate window is exhausted (10/hour)                      | **Retry after `error.details.retry_after`** (or the `Retry-After` header) with the same body.                         |
| **`CONFLICT` (409 duplicate label/extension)** | a create recycles a unique label or extension                        | **Branch on `error.code`.** Pick a new label or rotate the existing credential — shown above.                         |
| **`NOT_FOUND` (404)**                          | the `sipcred_*` id is stale, foreign, or soft-deleted                | **Re-list and re-branch.** Do not retry the id — shown above.                                                         |
