> ## 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 send-price and country gates (MAX_PRICE_EXCEEDED, COUNTRY_NOT_ALLOWED)

> Two pre-send 422 refusals that look alike on the wire but have different owners — the per-send max_price ceiling on the messaging payload, and the org-wide country allowlist. Decode which gate fired, read the details block, and apply the one fix that turns the retry into a success.

# 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_ALLOWED`** is 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 is `GET` / `PUT
  /api/v1/settings/compliance/country-allowlist` (dashboard: **Settings →
  Compliance → country allowlist**).
* **`MAX_PRICE_EXCEEDED`** is a **per-send cost ceiling**. It lives on the
  individual request payload as the `max_price` field and applies only to
  the send that carries it.

This page answers: **which gate fired, why it fired, and the exact fix
that turns a refused send into an accepted one.**

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

## Decode the envelope

Match `error.code` to a row. The `details` block tells you exactly which
side of the comparison the send fell on.

| Code | Where it fires | Cause | The named fix |
| - | - | - | - |
| `COUNTRY_NOT_ALLOWED` | Pre-send, on the outbound phone-destination path | Your organization set an explicit `allowed_countries` list, and the destination's resolved country is not on it. Fail-closed only once the list is set: with no list, no allow-all flag, or an unresolvable destination, the send passes. | Add the destination's ISO-3166-1 alpha-2 code to the list, or set `allow_all_countries: true` to record an open posture. Owner-only write. |
| `MAX_PRICE_EXCEEDED` | Pre-send, only on requests that carry `max_price` | The projected total cost of the send — the resolved per-unit rate × the counted segments for SMS/MMS, per-message otherwise — exceeds your `max_price` cap. | Compare `details.resolved_cost_usd` against your cap. Raise `max_price`, drop the field, shorten the body to fewer segments, or switch to a cheaper destination or channel. |

The scope of each gate is the other half of the diagnosis:

* `COUNTRY_NOT_ALLOWED` checks the **destination E.164 after
  normalization** — the resolved country of the `to` value, 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_EXCEEDED` checks only the **one request that carried
  `max_price`**. No `max_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:

```bash theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/settings/compliance/country-allowlist" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "allowed_countries": ["US", "CA"],
    "allow_all_countries": false
  },
  "meta": { "request_id": "req_5c1af2" }
}
```

**Fix at claim time — two resolutions:**

1. **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:
   ```bash theme={null}
   curl -X PUT "https://api.orbit.devotel.io/api/v1/settings/compliance/country-allowlist" \
     -H "X-API-Key: dv_live_sk_your_key_here" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: country-open-2026-09-29" \
     -d '{"allowed_countries": [], "allow_all_countries": true}'
   ```
2. **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:
   ```bash theme={null}
   curl -X PUT "https://api.orbit.devotel.io/api/v1/settings/compliance/country-allowlist" \
     -H "X-API-Key: dv_live_sk_your_key_here" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: country-add-es-2026-09-29" \
     -d '{"allowed_countries": ["US", "CA", "ES"]}'
   ```

The same panel is in the dashboard at **Settings → Compliance → country
allowlist** — the write is **owner-only** either way. After the change,
retry the send once; it dispatches.

<Warning>
  The gate checks the destination after E.164 normalization. A `to` value
  that normalizes to a country outside your list refuses even when the raw
  digits looked domestic — confirm the normalized destination before
  deciding the list is wrong. Equally, a destination that cannot be
  resolved to a country at all **passes**: resolution failure never
  blocks. Do not treat the allowlist as a substitute for the IRSF prefix
  blocks or Fraud Shield; it composes with them, it does not replace them.
</Warning>

**Never retry unchanged.** The reject is deterministic: the same
destination against the same list refuses identically until the list
changes, and a retry loop only burns your rate-limit budget.

## `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:

```json theme={null}
{
  "error": {
    "code": "MAX_PRICE_EXCEEDED",
    "message": "Projected total cost (0.028 USD = 0.007 USD × 4 segments) exceeds the caller's max_price cap (0.02 USD). Increase max_price, shorten the body, or pick a cheaper destination/channel.",
    "status": 422,
    "details": {
      "field": "max_price",
      "resolved_cost_usd": 0.028,
      "rate_per_unit_usd": 0.007,
      "segments": 4,
      "max_price_usd": 0.02,
      "channel": "sms"
    }
  },
  "meta": { "request_id": "req_77d0a1" }
}
```

**Fix at claim time — compare the quote to the cap, then pick one:**

`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:

1. **Raise the cap** — re-issue with a `max_price` above
   `resolved_cost_usd`. This is the intended fix when the cap was a
   conservative default and the quote is legitimate.
2. **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.)
3. **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](/concepts/sms-segments-and-encoding) for
   the counting model.
4. **Change the lane** — a cheaper destination or channel lowers the
   rate leg of the projection.

Then retry once — the corrected request returns the normal accepted
response. **Never retry unchanged:** without one of the four fixes, the
same projection exceeds the same cap identically.

## Decision checklist

1. Read `error.code` off the envelope.
2. **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 set `allow_all_countries: true`.
   Owner-only. Retry once.
3. **If `MAX_PRICE_EXCEEDED`:** read `details.resolved_cost_usd` vs
   `details.max_price_usd`; raise the cap, drop the field, shorten the
   body, or change the lane. Retry once.
4. 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_ALLOWED` with `CHANNEL_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_price` as 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-allowlist` output.
* For the price gate: the `details` block (`resolved_cost_usd`,
  `rate_per_unit_usd`, `segments`, `max_price_usd`, `channel`).

## See also

* [Error codes reference](/reference/error-codes) — the canonical
  rows for `MAX_PRICE_EXCEEDED` and `COUNTRY_NOT_ALLOWED`.
* [Outbound country allowlist — the model](/concepts/country-allowlist-gate-model) —
  why the gate is tenant-owned, fail-open until you opt in, and how it
  composes with the fraud stack.
* [Settings API reference](/api-reference/endpoints/settings) — the
  GET/PUT `/api/v1/settings/compliance/country-allowlist` request and
  response shapes.
* [Messaging endpoint — `max_price`](/api-reference/endpoints/messaging) —
  the per-send ceiling parameter on the send request.
* [SMS destination blocks](/troubleshooting/sms-destination-blocks) —
  the broader destination-gate family this allowlist sits beside.
* [SMS segments and encoding](/concepts/sms-segments-and-encoding) —
  the segment-counting model the price projection multiplies.
