> ## 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 pricing samples

> The pricing chains an integration runs before every send decision — the pre-send estimate, the published and effective rate cards, the what-if replay against a candidate rate card, and the operator-level SMS card — plus the errors worth branching on.

## Worked pricing samples

The generated blocks below document every parameter and response shape; this
overlay walks the three chains a tenant actually runs before a send decision:
**estimate one destination → read the effective rate sheet → replay traffic
against a candidate rate card** (the what-if call every sender runs before
committing volume), **read the current rate card → audit a rate change →
explain the delta on a bill**, and the **operator-level SMS rate card with
scope filters** team admins use to compare organizations. Every response is
the real `{ data, meta }` envelope — `data` carries the payload, `meta`
carries the `request_id` and `timestamp`. Quote `meta.request_id` when you
report a bad number.

The Node SDK does not wrap the pricing namespace yet (`/pricing/rates` in the
SDK targets the legacy per-country table, not these tenant routes), so the
typed tabs below use the SDK's `orbit.request` escape hatch — you get the
SDK's auth, retries, and envelope unwrapping against the raw REST path.
Python fronts `client.request` the same way.

### 1. Estimate one destination before you send

`GET /api/v1/pricing/estimate` resolves a single send to the per-unit rate
the wallet would actually be charged. Pass `channel` and a destination `to`
in E.164 form — the endpoint resolves the destination country itself. Add
`allIn=true` to stack the hidden-fee line items (US A2P 10DLC registration,
call recording, transcription, premium support) a "what does this lane
really cost" question needs; `include` narrows that list to named surcharge
codes.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/pricing/estimate?channel=sms&to=%2B14155550123&allIn=true" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const estimate = await orbit.request(
    "GET",
    "/api/v1/pricing/estimate?channel=sms&to=%2B14155550123&allIn=true",
  );
  // estimate.data.rate — { ratePerUnit, currency, countryCode }
  // estimate.data.allIn.surcharges — itemized hidden fees on the lane
  ```

  ```python Python theme={null}
  estimate = client.request(
      "GET",
      "/api/v1/pricing/estimate",
      params={"channel": "sms", "to": "+14155550123", "allIn": "true"},
  )
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "channel": "sms",
    "to": "+14155550123",
    "countryCode": "US",
    "rate": {
      "ratePerUnit": "0.0075",
      "currency": "USD",
      "countryCode": "US"
    },
    "allIn": {
      "baseRatePerUnit": "0.0075",
      "currency": "USD",
      "surcharges": [
        {
          "code": "a2p_10dlc_registration",
          "label": "US A2P 10DLC registration",
          "amountUsd": 4.5,
          "billingPeriod": "monthly"
        }
      ]
    }
  },
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:00.000Z"
  }
}
```

### 2. Verify against your effective rate sheet

`GET /api/v1/pricing/effective-rates` is the tenant-facing read the what-if
and estimate checks reconcile against: the global platform card collapsed
with any per-org overrides down to the single row the billing resolver would
charge per (channel, sub-type, country, direction) lane. Each row shows the
bare base rate and the effective per-unit price in cents after overrides
(and, for SMS, the platform default markup) are layered on — so the number
you display cannot drift from the number the wallet debits. `hasCustomRate`
is set only when your own org carried the override, never for the platform
default.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/pricing/effective-rates" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const sheet = await orbit.request("GET", "/api/v1/pricing/effective-rates");
  // sheet.data — one row per billable lane
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": [
    {
      "channel": "sms",
      "subType": null,
      "countryCode": "US",
      "direction": "mt",
      "currency": "USD",
      "baseRatePerUnit": 0.0075,
      "effectivePerUnitCents": 0.825,
      "provider": "devotel",
      "hasCustomRate": false
    }
  ],
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:02.000Z"
  }
}
```

### 3. Replay traffic against a candidate rate card (the what-if)

`POST /api/v1/pricing/whatif-simulate` is the call you run before committing
volume: it replays a window of your own observed traffic (one entry per
billable lane, in `usage`) against a candidate rate card
(`candidateRates`) and projects the cost side-by-side with what the traffic
was actually billed. Traffic is held fixed; only the rate card is swapped —
that is the what-if. It is read-only: nothing is written, nothing is sent,
and an `INSUFFICIENT_BALANCE` at send time later means the top-up reminder
was never a reason to skip this preview. Both arrays fill a real body — a
bare `{}` returns 422.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.orbit.devotel.io/api/v1/pricing/whatif-simulate" \
    -H "X-API-Key: $ORBIT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "usage": [
      { "channel": "sms", "countryCode": "US", "direction": "mt", "units": 12000, "currentBilledUsd": 96.40 },
      { "channel": "sms", "countryCode": "GB", "direction": "mt", "units": 4100, "currentBilledUsd": 58.10 }
    ],
    "candidateRates": [
      { "channel": "sms", "countryCode": "US", "direction": "mt", "ratePerUnit": 0.0075 },
      { "channel": "sms", "countryCode": "GB", "direction": "mt", "ratePerUnit": 0.012 },
      { "channel": "sms", "countryCode": "*", "direction": "mt", "ratePerUnit": 0.015 }
    ]
  }'
  ```

  ```typescript Node.js theme={null}
  const whatIf = await orbit.request("POST", "/api/v1/pricing/whatif-simulate", {
    usage: [
      { channel: "sms", countryCode: "US", direction: "mt", units: 12000, currentBilledUsd: 96.4 },
      { channel: "sms", countryCode: "GB", direction: "mt", units: 4100, currentBilledUsd: 58.1 },
    ],
    candidateRates: [
      { channel: "sms", countryCode: "US", direction: "mt", ratePerUnit: 0.0075 },
      { channel: "sms", countryCode: "GB", direction: "mt", ratePerUnit: 0.012 },
      { channel: "sms", countryCode: "*", direction: "mt", ratePerUnit: 0.015 },
    ],
  });
  // whatIf.data.totalDeltaUsd — negative = the candidate card is cheaper
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "lanes": [
      {
        "channel": "sms",
        "countryCode": "US",
        "direction": "mt",
        "subType": null,
        "units": 12000,
        "matchedRatePerUnit": 0.0075,
        "matchedCountryCode": "US",
        "projectedUsd": 90,
        "currentBilledUsd": 96.4,
        "deltaUsd": -6.4,
        "unmatched": false
      },
      {
        "channel": "sms",
        "countryCode": "GB",
        "direction": "mt",
        "subType": null,
        "units": 4100,
        "matchedRatePerUnit": 0.012,
        "matchedCountryCode": "GB",
        "projectedUsd": 49.2,
        "currentBilledUsd": 58.1,
        "deltaUsd": -8.9,
        "unmatched": false
      }
    ],
    "totalUnits": 16100,
    "totalProjectedUsd": 139.2,
    "totalCurrentBilledUsd": 154.5,
    "totalDeltaUsd": -15.3,
    "deltaPct": -9.9,
    "matchedLaneCount": 2,
    "unmatchedLaneCount": 0
  },
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:04.000Z"
  }
}
```

A lane no candidate row covers comes back with `"unmatched": true` and
`"matchedRatePerUnit": null` — read `unmatchedLaneCount` before you trust
`totalProjectedUsd`; a candidate card with coverage gaps under-projects.

### 4. Read the current rate card (admin)

`GET /api/v1/pricing/rates` is the super-admin read of the full platform
rate card — every lane, global defaults and per-org rows alike, ordered by
channel then country. Use it before you edit or retire a lane; the list is
capped at 5,000 rows.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/pricing/rates" \
    -H "X-API-Key: $ORBIT_ADMIN_KEY"
  ```

  ```typescript Node.js theme={null}
  const card = await orbit.request("GET", "/api/v1/pricing/rates");
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": [
    {
      "id": "rate_01HZY8EXAMPLE",
      "organizationId": null,
      "channel": "sms",
      "subType": null,
      "countryCode": "US",
      "direction": "mt",
      "ratePerUnit": "0.0075",
      "currency": "USD",
      "provider": "devotel",
      "costAmount": null,
      "costCurrency": null,
      "markupBps": null,
      "source": "seed",
      "sourceRef": null,
      "createdBy": "migration",
      "effectiveFrom": "2026-01-01T00:00:00.000Z",
      "effectiveTo": null,
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ],
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:06.000Z"
  }
}
```

### 5. Audit what a rate change did to a tenant's bill

`GET /api/v1/pricing/changelog` answers "when did my pricing change, and
what changed". A super-admin sees the raw internal audit rows (actor,
reason, before/after snapshots) and may filter by `organizationId`; a tenant
API key gets the sanitized projection — which surface changed
(`rate_card` or `org_override`), the action, whether it was a global
rate-card change or org-scoped, and the timestamp — with the internal
fields stripped, and scope pinned to the caller's own org plus global
changes. A tenant-supplied `organizationId` is ignored (no cross-tenant
read). Cap with `limit` (default 100, max 500).

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

  ```typescript Node.js theme={null}
  const changes = await orbit.request(
    "GET",
    "/api/v1/pricing/changelog?limit=50",
  );
  ```
</CodeGroup>

Tenant view (sanitized projection):

```json 200 theme={null}
{
  "data": [
    {
      "id": "chg_01HZY8EXAMPLE",
      "resource": "org_override",
      "action": "update",
      "scope": "organization",
      "organizationId": "org_01HZY8ORG",
      "changedAt": "2026-09-08T15:42:11.000Z"
    },
    {
      "id": "chg_01HZY8EXAMP2",
      "resource": "rate_card",
      "action": "create",
      "scope": "global",
      "organizationId": null,
      "changedAt": "2026-09-01T09:00:00.000Z"
    }
  ],
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:08.000Z"
  }
}
```

Super-admins get the raw rows back instead, with the operator `changedBy`, a
free-text `reason`, and the full `beforeState` / `afterState` snapshots —
shape preserved verbatim, internal fields included. Pair the tenant view
with `/pricing/effective-rates` to explain a line on a bill; pair the
super-admin view with `/pricing/overrides?organizationId=...` to scope the
audit to one tenant.

### 6. Operator-level SMS rate card with scope filters

`GET /api/v1/pricing/mccmnc-rates` is the customer-facing SMS rate card at
mobile-operator (MCCMNC) granularity, with your organization's markup
already applied. Filter with `search` (operator name, country, or MCCMNC
code) and `country`, and page with `cursor` / `limit`. A row with no
published cost returns `"your_rate": null` — the "Contact sales" row — and
`pricing_source` attributes each row to the same rule the send-path resolver
bills under (`override` / `org_markup` / `default_markup`), so the preview
cannot quote a price the wallet would not charge.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/pricing/mccmnc-rates?search=Cosmote&limit=50" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const operators = await orbit.request(
    "GET",
    "/api/v1/pricing/mccmnc-rates?search=Cosmote&limit=50",
  );
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": [
    {
      "mccmnc": "20201",
      "operator_name": "Cosmote",
      "country": "GR",
      "your_rate": 0.0462,
      "currency": "USD",
      "pricing_source": "default_markup"
    }
  ],
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:10.000Z",
    "pagination": {
      "cursor": "20201",
      "has_more": false
    }
  }
}
```

The same super-admin scope filters the picker and override surfaces use —
`/pricing/organizations` (org rows with a `hasCustomRates` flag, `all=true`
for one round trip) and `/pricing/overrides?organizationId=org_...` — let a
team admin compare a tenant's negotiated rates against the platform card
before answering a dispute.

### 7. Public price list (no key needed)

`GET /api/v1/public/pricing` publishes the platform pay-as-you-go price list
with the pricing-model manifest — one tier, no contact-sales gate, no forced
migrations. It is unauthenticated and IP rate-limited, so a prospect can
verify pricing before signing up. Pass `?country=US` for the published
country-specific list; omit it for the baseline.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/public/pricing?country=US"
  ```

  ```typescript Node.js theme={null}
  const published = await orbit.request(
    "GET",
    "/api/v1/public/pricing?country=US",
  );
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "model": {
      "type": "pay_as_you_go",
      "singleTier": true,
      "planTiers": [],
      "contactSalesGated": false,
      "forcedMigrations": false,
      "currency": "USD",
      "note": "Single transparent pay-as-you-go model for every account — no plan tiers, no contact-sales gate, and no forced pricing-model migrations. You only pay published per-unit rates for what you send."
    },
    "country": "US",
    "channels": [
      {
        "channel": "sms",
        "unit": "per_segment",
        "ratePerUnit": 0.0165,
        "currency": "USD",
        "billed": true,
        "source": "platform-default"
      },
      {
        "channel": "voice",
        "unit": "per_minute",
        "ratePerUnit": 0.014,
        "currency": "USD",
        "billed": true,
        "source": "platform-default"
      }
    ]
  },
  "meta": {
    "request_id": "req_01HZY8EXAMPLE",
    "timestamp": "2026-09-10T12:00:12.000Z"
  }
}
```

SMS is metered per **segment** (`per_segment`), not per message: a GSM-7
body splits every 160 characters and a UCS-2 (unicode) body every 70, so a
long send costs N× the per-segment rate. Voice bills per minute, fax per
page. Bring-your-own-credential channels (WhatsApp, Instagram, Messenger,
push, Apple Messages, LINE) are listed with `"billed": false` at \$0 — the
platform only charges for what it terminates itself.

### 8. Errors worth branching on

The envelope mechanics — `error.code` / `message` / `status` / `details`,
retry order, retriable-vs-terminal — live once in the
[error-handling guide](/guides/error-handling-examples).
Here is what the branch decision on THIS page comes down to.

<ResponseExample>
  ```json 422 theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Invalid what-if simulator input",
      "status": 422,
      "details": { "issues": [{ "field": "usage", "message": "Required" }] }
    },
    "meta": { "request_id": "req_01HZY8EXAMPLE", "timestamp": "2026-09-10T12:00:14.000Z" }
  }
  ```
</ResponseExample>

<ResponseExample>
  ```json 403 theme={null}
  {
    "error": {
      "code": "FORBIDDEN",
      "message": "Super admin access required",
      "status": 403
    },
    "meta": { "request_id": "req_01HZY8EXAMPLE", "timestamp": "2026-09-10T12:00:16.000Z" }
  }
  ```
</ResponseExample>

| Class                                      | Meaning                                                                                                                                                    | Branch response                                                                                                                                                                                                |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **422 `VALIDATION_ERROR`**                 | A bare `{}` body on either simulator, a missing/malformed `country`, or `channel`/`to` omitted on estimate. Typed out-of-range input the API cannot price. | **Fix then resend.** The `details.issues` array names the field — correct the body/params, then re-issue. Never retry a 422 blind.                                                                             |
| **403 `FORBIDDEN`**                        | A tenant key presented against `/pricing/rates`, `/pricing/organizations`, or `/pricing/overrides` — the super-admin-only reads.                           | **Surface.** These are admin surfaces; the tenant-facing equivalents (`/pricing/effective-rates`, `/pricing/mccmnc-rates`) return 200. Do not retry against the admin path until the key is a super-admin key. |
| **429 `RATE_LIMITED`**                     | The auth-write bucket pricing reads draw from is exhausted. Reads are cheap; the bucket reset is fast.                                                     | **Retry after `error.details.retry_after`.** Public pricing draws from the public-IP bucket; estimate + simulators draw from the auth bucket.                                                                  |
| **401 `INVALID_API_KEY`**                  | Missing or revoked key. Present on every `/pricing/*` route except `/public/pricing`.                                                                      | **Fix, then re-key.** A tenant-scoped read with no valid key is a hard terminal — re-issue credentials, do not retry.                                                                                          |
| **402 `INSUFFICIENT_BALANCE`** (interplay) | A send that follows an estimate can still hit this at the wallet; the estimate endpoint itself never charges.                                              | **Top up, then resend the send.** The estimate response is a preview — a later `INSUFFICIENT_BALANCE` on the real send means the wallet ran dry between estimate and send, not that the estimate lied.         |

Treat `422` and `403` as terminal, not retriable — a corrected request fixes
the first; the second needs a super-admin key or a switch to the
tenant-facing read. `429` and `401` are retry-once-after-fix and
re-key-then-retry respectively. The pricing reads draw from the same
`RATE_LIMIT_AUTH_WRITE` bucket as the other authenticated reads, and the
public list has its own IP bucket — the envelope's
`error.details.retry_after` says how long.

These samples compound; the auto-generated per-operation entries below
always carry the full parameter table even where a sample here already
covers the op.
