> ## 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.

# Troubleshoot pricing-gate errors (PRICING_NOT_CONFIGURED, PRICING_FX_*, PRICING_RATE_*, PRICING_OVERRIDE_SLOT_CONFLICT, PRICING_UPLOAD_STALE_CONFLICT)

> A send or pricing write refused by a pricing-gate code in the Billing family — decode which gate fired, split tenant-owned overrides from platform-operator-owned FX and rate-card failures, and resolve the block with one named fix.

# Troubleshoot pricing-gate errors

Pricing gates sit on two paths: the **billing resolver** that prices every
send on the send path, and the **pricing console/API** that maintains the
rate card and per-org overrides. A refusal on either path carries one of
the `PRICING_*` codes from the Billing family. This runbook decodes each
code to one named fix, and splits the codes by owner: the ones a **tenant
admin** can clear (their own per-org override) from the ones only a
**platform operator** can clear (the rate card, FX snapshots, and
wholesale-cost guards — escalate those to the platform and include the
billing envelope).

This page answers: **which gate fired, who owns the fix, and the exact
resolution that turns a refused write into a success.**

<Note>
  Pricing gates fail **closed**: a send priced against a missing rate or an
  override refused by a slot conflict is never dispatched, and a refused
  override write never persists. Nothing bills in the rejected state — the
  wallet hold is released with the refusal.
</Note>

## Decode the code

Match `error.code` from the envelope to a row. The "Owner" column is the
split that tells you whether to fix it yourself or escalate.

| Code                                | Where it fires                                                                                                                                                     | Owner                | The named fix                                                                                                                                                                                                                                                                                                        |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PRICING_NOT_CONFIGURED`            | Send path — the billing resolver could not find a rate row **or** a per-org override for the `(channel, sub_type, country, mcc, mnc, prefix)` tuple being charged. | Platform operator    | The lane has no published rate. Dry-run the destination against `POST /api/v1/messaging/smpp/carriers/route-quote` to confirm the resolver returns no quote, then the operator publishes a rate for the lane (the tenant escalates with the exact channel/sub-type/country tuple). Never billed until a rate exists. |
| `PRICING_FX_NOT_CONFIGURED`         | Send path and FX warm — the currency-conversion leg is required but the FX provider key is unset.                                                                  | Platform operator    | The FX upstream credential is missing at the platform; the tenant escalates. No tenant-side workaround.                                                                                                                                                                                                              |
| `PRICING_FX_STALE`                  | Send path — the newest FX snapshot for the required currency pair is older than the 7-day hard ceiling.                                                            | Platform operator    | FX refresh job is behind or failing; the tenant escalates with the currency pair from `error.details`.                                                                                                                                                                                                               |
| `PRICING_FX_UPSTREAM_FAILED`        | FX warm — the FX provider returned a non-2xx while warming a missing rate snapshot.                                                                                | Platform operator    | Transient upstream outage — retry once after a minute; if it persists, escalate (the platform's provider status page is the source of truth).                                                                                                                                                                        |
| `PRICING_FX_UNEXPECTED_BASE`        | FX warm — the provider returned a base currency other than USD.                                                                                                    | Platform operator    | Provider contract breach; escalate — the platform treats USD-base as an invariant.                                                                                                                                                                                                                                   |
| `PRICING_FX_UNSUPPORTED_CURRENCY`   | Send path or override write — the requested currency is outside the supported set.                                                                                 | Caller               | Fix the payload: re-issue with a supported currency (check `error.details.requested` if present). Never retried as-is.                                                                                                                                                                                               |
| `PRICING_RATE_BELOW_WHOLESALE_COST` | Override create (`POST /api/v1/admin/pricing/overrides`) — the absolute price is below the platform's active wholesale cost for the lane.                          | Tenant-owned payload | Raise the override price to at least the wholesale cost in `error.details.wholesaleRatePerUnit`, or drop the absolute price and use `overrideMarkupBps` instead.                                                                                                                                                     |
| `PRICING_OVERRIDE_SLOT_CONFLICT`    | Override create — a second **overlapping** active override on the exact same `(organization, channel, sub-type, country, direction)` scope.                        | Tenant-owned payload | Edit the existing override (`PATCH /api/v1/admin/pricing/overrides/:id` — the id is in `error.details.existingOverrideId`), or end-date the current window before the new one starts. A clean back-to-back handoff (old `effectiveTo` equals new `effectiveFrom`) does **not** conflict.                             |
| `PRICING_RATE_SANITY_VIOLATION`     | Rate create/patch — the new cost moved more than 5× above, or below 20% of, the lane's active cost.                                                                | Platform operator    | The operator revisits the intended rate — the guard exists to stop a mistyped decimal from repricing a country.                                                                                                                                                                                                      |
| `PRICING_RATE_SLOT_CONFLICT`        | Rate create/patch — collides with an already-active rate on the same `(channel, country, direction, sub-type)` slot.                                               | Platform operator    | The operator edits or supersedes the colliding active rate instead of inserting a second row into the same slot.                                                                                                                                                                                                     |
| `PRICING_UPLOAD_STALE_CONFLICT`     | Rate-file apply — the cached preview went stale before it was applied (another apply changed the rate card in the window).                                         | Platform operator    | Re-run the preview and apply the fresh upload; never re-apply the same cached preview — it collides with the slot that changed.                                                                                                                                                                                      |

## Tenant-owned vs platform-operator-owned

The owner split is the most common escalation mistake. Route the ticket
correctly and the fix is one step:

* **Tenant admin owns** the per-org **override payloads**: an absolute
  price that dips below wholesale cost (`PRICING_RATE_BELOW_WHOLESALE_COST`),
  a second overlapping override on the same scope (`PRICING_OVERRIDE_SLOT_CONFLICT`),
  and an unsupported currency on the request (`PRICING_FX_UNSUPPORTED_CURRENCY`).
  These are payload fixes — adjust the request and retry.
* **Platform operator owns** everything upstream of the tenant: the rate-card
  rows (`PRICING_NOT_CONFIGURED`, `PRICING_RATE_SLOT_CONFLICT`,
  `PRICING_RATE_SANITY_VIOLATION`, `PRICING_UPLOAD_STALE_CONFLICT`) and the
  FX chain (`PRICING_FX_NOT_CONFIGURED`, `PRICING_FX_STALE`,
  `PRICING_FX_UPSTREAM_FAILED`, `PRICING_FX_UNEXPECTED_BASE`). The tenant has
  no control surface for these — capture the envelope and escalate.

## Worked example — a stuck 422 on the send path

A send to a lane with no published rate refuses with `PRICING_NOT_CONFIGURED`.
Before opening a ticket, dry-run the destination against the route-quote
endpoint — it runs the same resolver the send path uses, without billing
anything:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messaging/smpp/carriers/route-quote \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"destination": "+34612345678", "messageCount": 1}'
```

```json theme={null}
{
  "data": {
    "decision": "devotel",
    "devotelQuote": null
  },
  "meta": { "request_id": "req_9f2ac1" }
}
```

`devotelQuote: null` is the tell: the resolver could not price the
destination, so the live send would refuse with `PRICING_NOT_CONFIGURED`
the same way. A configured lane returns a `unitPriceCents`/`totalCents`
estimate instead. When the dry run is null, escalate with the destination,
the channel, and the `request_id` — the operator publishes the missing
rate and the dry run starts returning a quote.

## Worked example — an override refused by the wholesale-cost guard

You set an absolute per-message price of `0.002` USD-cents denominated
units on a lane whose wholesale cost is `0.0075` per unit. The create
refuses because every message billed under that override would charge the
org less than the platform pays upstream:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/admin/pricing/overrides \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "org_8cd21f",
    "channel": "sms",
    "countryCode": "ES",
    "direction": "mt",
    "overrideCostAmount": 0.002,
    "reason": "Promotional rate for Q3 campaign"
  }'
```

```json theme={null}
{
  "error": {
    "code": "PRICING_RATE_BELOW_WHOLESALE_COST",
    "message": "Override price 0.002 is below the platform's active wholesale cost 0.0075 (USD) for sms/ES — this would bill the org LESS than Orbit pays upstream on every unit.",
    "status": 422,
    "details": {
      "organizationId": "org_8cd21f",
      "channel": "sms",
      "countryCode": "ES",
      "overrideCostAmount": 0.002,
      "wholesaleRatePerUnit": 0.0075
    }
  },
  "meta": { "request_id": "req_51bb02" }
}
```

**Resolution:** pick one of the three fixes — raise `overrideCostAmount`
to at least `wholesaleRatePerUnit`, swap the absolute price for a markup
(`overrideMarkupBps`, which prices above cost by construction), or narrow
the scope (`subType`, `direction`) to the lane you actually negotiated.
Then retry the same create — it returns `201` with the persisted override.
Wholesale cost is a platform-published input; if your agreement prices a
lane below it, that is negotiated out-of-band and applied by the operator,
not through this endpoint.

## Worked example — a second override refused by the slot conflict

A scope already has an active override, and a second create on the exact
same scope with an overlapping window refuses with a `409`:

```json theme={null}
{
  "error": {
    "code": "PRICING_OVERRIDE_SLOT_CONFLICT",
    "message": "An override already exists for org \"org_8cd21f\" on this channel/scope (channel \"sms\", country \"ES\", direction \"mt\") whose effective window overlaps this one — id=pricingOverride_7ab3d1. Edit that override (PATCH /admin/pricing/overrides/pricingOverride_7ab3d1) instead of creating a second one, or adjust the effective dates so the two don't overlap; otherwise the resolver would silently pick between them by latest effective date.",
    "status": 409,
    "details": {
      "organizationId": "org_8cd21f",
      "channel": "sms",
      "countryCode": "ES",
      "direction": "mt",
      "existingOverrideId": "pricingOverride_7ab3d1"
    }
  },
  "meta": { "request_id": "req_77e10c" }
}
```

**Resolution — three ways out, one retry:**

1. **Edit in place** — `PATCH /api/v1/admin/pricing/overrides/pricingOverride_7ab3d1`
   with the new price or markup. Use the id from `error.details.existingOverrideId`.
2. **Re-scope** — add or change a scope field (`subType` or `direction`)
   so the new override covers a different lane.
3. **Schedule a handoff** — end-date the current override and start the
   new one after: set `effectiveTo` on the existing row (PATCH), then
   create the new override with `effectiveFrom` equal to that instant.
   Half-open windows are exclusive at the boundary, so a back-to-back
   handoff is not a conflict.

Any of the three turns the next `POST` into a `201` with the override
row in the response body. Deleting the conflicting override and
re-creating it also clears the slot, but the PATCH keeps the audit trail
intact and is the preferred fix.

## Decision checklist

1. Read `error.code` and `error.status` off the envelope; match the row in
   the decode table above.
2. Check the **Owner** column — a tenant-owned row is a payload fix; an
   operator row is an escalation.
3. For a send-path refusal, dry-run `POST /api/v1/messaging/smpp/carriers/route-quote`
   for the destination and capture the `devotelQuote` null-vs-priced result.
4. For an override refusal, take `error.details.existingOverrideId` /
   `wholesaleRatePerUnit` as the ground truth for the fix, adjust the
   payload, and retry once.
5. If the row names the operator, escalate with the envelope (see below).

## Full-error sample (paste-ready)

A send refused by the resolver on an unpriced lane:

```json theme={null}
{
  "error": {
    "code": "PRICING_NOT_CONFIGURED",
    "message": "No pricing rate or org override resolves for sms/mt/ES — the lane has no published rate.",
    "status": 422,
    "details": {
      "channel": "sms",
      "direction": "mt",
      "countryCode": "ES"
    }
  },
  "meta": {
    "request_id": "req_4ad90e",
    "timestamp": "2026-09-20T10:15:42Z"
  }
}
```

## What NOT to do

* **Do not blind-retry** a `PRICING_*` refusal. Every gate in this family
  is deterministic — the same payload refuses identically until the named
  fix lands, and a retry loop burns your rate-limit budget.
* **Do not re-apply a stale rate-file preview** after
  `PRICING_UPLOAD_STALE_CONFLICT`. Re-run the preview against the current
  rate card; the stale batch collides with the rows that changed.
* **Do not delete-and-recreate overrides in a loop** to dodge the slot
  conflict — the guard compares effective windows, and an overlapping
  recreation refuses the same way. PATCH the existing row or schedule the
  handoff.
* **Do not treat a `PRICING_FX_*` send-path refusal as tenant-fixable.**
  The FX chain is platform-owned; re-sending the same payload while the
  operator fixes it only re-fails. Escalate once, then retry after the
  all-clear.
* **Do not scope an override by MCC/MNC/prefix** — the resolver scopes
  overrides by channel, sub-type, country, and direction only, and the
  request refuses with a 422 validation error rather than silently
  broadening the override.

## When to escalate

Escalate when the decode row's **Owner** column says platform operator,
or when a tenant-owned fix still refuses after the payload change named
by `error.details`. Include:

* Your **tenant ID** (`GET /api/v1/me` → `organizationId`).
* The **exact code** and the **request ID** (`meta.request_id`) from one
  refused envelope.
* For a send-path refusal: the **destination**, **channel**, and the
  route-quote dry-run output (especially `devotelQuote: null`).
* For an override refusal: the **override payload** you sent and the
  `existingOverrideId` / `wholesaleRatePerUnit` from `error.details`.
* For FX codes: the **currency pair** and the observed time window.

## See also

* [Error codes reference — Billing family](/reference/error-codes#billing) —
  the canonical descriptions for every `PRICING_*` row.
* [Spend-cap refusals](/troubleshooting/spend-caps-hit) — the tenant-owned
  daily ceilings, a different 429 family from these gates.
* [INSUFFICIENT\_BALANCE](/troubleshooting/insufficient-balance) — the
  wallet pre-flight the resolver runs before any pricing gate.
* [Idempotency and billing gates](/troubleshooting/idempotency-and-billing-gates) —
  the deduct-in-flight and idempotency guards adjacent to the resolver.
