Skip to main content

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

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.

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:
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:
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:
Resolution — three ways out, one retry:
  1. Edit in placePATCH /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:

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/meorganizationId).
  • 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