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 thePRICING_* 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.
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.
Decode the code
Matcherror.code from the envelope to a row. The “Owner” column is the
split that tells you whether to fix it yourself or escalate.
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 withPRICING_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:
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 of0.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:
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 a409:
- Edit in place —
PATCH /api/v1/admin/pricing/overrides/pricingOverride_7ab3d1with the new price or markup. Use the id fromerror.details.existingOverrideId. - Re-scope — add or change a scope field (
subTypeordirection) so the new override covers a different lane. - Schedule a handoff — end-date the current override and start the
new one after: set
effectiveToon the existing row (PATCH), then create the new override witheffectiveFromequal to that instant. Half-open windows are exclusive at the boundary, so a back-to-back handoff is not a conflict.
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
- Read
error.codeanderror.statusoff the envelope; match the row in the decode table above. - Check the Owner column — a tenant-owned row is a payload fix; an operator row is an escalation.
- For a send-path refusal, dry-run
POST /api/v1/messaging/smpp/carriers/route-quotefor the destination and capture thedevotelQuotenull-vs-priced result. - For an override refusal, take
error.details.existingOverrideId/wholesaleRatePerUnitas the ground truth for the fix, adjust the payload, and retry once. - 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: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 byerror.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/wholesaleRatePerUnitfromerror.details. - For FX codes: the currency pair and the observed time window.
See also
- Error codes reference — Billing family —
the canonical descriptions for every
PRICING_*row. - Spend-cap refusals — the tenant-owned daily ceilings, a different 429 family from these gates.
- INSUFFICIENT_BALANCE — the wallet pre-flight the resolver runs before any pricing gate.
- Idempotency and billing gates — the deduct-in-flight and idempotency guards adjacent to the resolver.