Troubleshoot send-price and country gates
MAX_PRICE_EXCEEDED and COUNTRY_NOT_ALLOWED both refuse a send at
pre-send with HTTP 422, before the wallet is billed — but they are
different controls answering to different owners:
COUNTRY_NOT_ALLOWEDis the org-wide country allowlist gate. It lives on your organization’s compliance settings, applies to every SMS, messaging-channel, and voice send with a phone destination, and the control surface isGET/PUT /api/v1/settings/compliance/country-allowlist(dashboard: Settings → Compliance → country allowlist).MAX_PRICE_EXCEEDEDis a per-send cost ceiling. It lives on the individual request payload as themax_pricefield and applies only to the send that carries it.
Both gates fail closed before dispatch: nothing is sent and nothing is
billed in the rejected state. Neither code is transient — until the named
fix lands, retrying the same payload refuses identically.
Decode the envelope
Matcherror.code to a row. The details block tells you exactly which
side of the comparison the send fell on.
The scope of each gate is the other half of the diagnosis:
COUNTRY_NOT_ALLOWEDchecks the destination E.164 after normalization — the resolved country of thetovalue, for every send in the organization. A provisional phone format that normalizes to a different country than you expected is a common surprise.MAX_PRICE_EXCEEDEDchecks only the one request that carriedmax_price. Nomax_price, no gate.
COUNTRY_NOT_ALLOWED — the org allowlist rejected the destination
The country allowlist is a tenant-owned opt-in gate: until you configure
it, all destinations are allowed by default. Once you write a
non-empty allowed_countries list, only destinations that resolve to a
listed ISO-3166-1 alpha-2 code pass — everything else is hard-rejected
with 422 COUNTRY_NOT_ALLOWED. The allow_all_countries: true flag
exists so “we intentionally send anywhere” is a recorded decision, not
an empty list someone hopes stays empty.
Read the current posture before changing it:
- Open the gate — if the org posture is “send anywhere”, record it
explicitly with
allow_all_countries: true(the flag short-circuits before the list is consulted), or clear the list: - Add the ISO codes you actually need — the PUT replaces the list
wholesale, so it must carry every country you intend to keep, plus the
new one. Codes are upper-cased and deduplicated server-side:
MAX_PRICE_EXCEEDED — the per-send price ceiling rejected the quote
max_price is an optional field on the messaging send request (POST /api/v1/messages) — a hard USD ceiling you set on that one call, capped
at 100 USD per message to defend against unit mistakes. When the guard’s
projected total cost exceeds your cap, the send refuses with
422 MAX_PRICE_EXCEEDED. SMS and MMS are priced per segment, so the
projection multiplies the resolved per-unit rate by the counted segments
of the body — a body that re-encoded into UCS-2 can trip a cap the
GSM-7 version of the same text passed.
A refused envelope looks like this:
details.resolved_cost_usd is the projected total the guard computed
(rate × segments); details.max_price_usd is the ceiling you sent. One
of four resolutions fits every refusal:
- Raise the cap — re-issue with a
max_priceaboveresolved_cost_usd. This is the intended fix when the cap was a conservative default and the quote is legitimate. - Drop the field — if you do not need a per-send ceiling on this
lane, omit
max_price; the guard only runs when the field is present. (Org-wide spend discipline lives elsewhere: daily spend caps and fraud rules.) - Shorten the body — for SMS/MMS the projection scales with segment count, so trimming an emoji-flipped or UCS-2 body back into GSM-7 can collapse it from multiple segments to one. See SMS segments and encoding for the counting model.
- Change the lane — a cheaper destination or channel lowers the rate leg of the projection.
Decision checklist
- Read
error.codeoff the envelope. - If
COUNTRY_NOT_ALLOWED: GET the current allowlist, confirm the destination’s normalized country, and either add its ISO code (PUT with the full intended list) or setallow_all_countries: true. Owner-only. Retry once. - If
MAX_PRICE_EXCEEDED: readdetails.resolved_cost_usdvsdetails.max_price_usd; raise the cap, drop the field, shorten the body, or change the lane. Retry once. - If the fix lands and the code still refuses, capture the envelope and escalate (below).
What NOT to do
- Do not blind-retry either code. Both gates are deterministic — unchanged input, unchanged verdict — and a retry loop only burns your rate-limit budget.
- Do not PUT a partial country list. The allowlist PUT replaces the list wholesale; a body carrying only the new ISO code silently drops every country you omitted. Always send the full intended set.
- Do not confuse
COUNTRY_NOT_ALLOWEDwithCHANNEL_COUNTRY_BLOCKED. The allowlist is one org-wide list; channel-per-country blocks are a separate tenant policy surface. Check the code, not the 422. - Do not treat
max_priceas org-wide spend policy. It caps one message. For daily ceilings use spend caps; for unit mistakes know the field itself is capped at 100 USD.
When to escalate
Escalate only if a corrected request still refuses, or if you cannot update the allowlist as the owner. Include:- Your tenant ID (
GET /api/v1/me→organizationId). - The exact code, one full envelope, and
meta.request_id. - For the country gate: the normalized destination and the current
GET
/api/v1/settings/compliance/country-allowlistoutput. - For the price gate: the
detailsblock (resolved_cost_usd,rate_per_unit_usd,segments,max_price_usd,channel).
See also
- Error codes reference — the canonical
rows for
MAX_PRICE_EXCEEDEDandCOUNTRY_NOT_ALLOWED. - Outbound country allowlist — the model — why the gate is tenant-owned, fail-open until you opt in, and how it composes with the fraud stack.
- Settings API reference — the
GET/PUT
/api/v1/settings/compliance/country-allowlistrequest and response shapes. - Messaging endpoint —
max_price— the per-send ceiling parameter on the send request. - SMS destination blocks — the broader destination-gate family this allowlist sits beside.
- SMS segments and encoding — the segment-counting model the price projection multiplies.