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

# Troubleshooting: validation gate codes

> The deterministic validation rejects — INVALID_PHONE_NUMBER, MISSING_REQUIRED_FIELD, INVALID_FROM_NUMBER, INVALID_EMAIL — refuse a request before anything bills, queues, or dispatches. Decode the code, pre-flight the value with number lookup or an E.164 formatter, fix the field the envelope names, and re-send once.

# Troubleshooting: validation gate codes

Four codes share one behaviour: they refuse the request **deterministically,
before anything bills, queues, or reaches a carrier**. The payload is the
problem, never the platform — so a retry loop earns the same `422` every
time, and the only working fix is to change the value the envelope names.

| Code                     | What tripped                                                          | Where you see it                                                    |
| ------------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `INVALID_PHONE_NUMBER`   | A phone field is not valid E.164                                      | send endpoints, voice dial, flow send nodes, caller-ID registration |
| `INVALID_FROM_NUMBER`    | The `from_number` source id passed is missing, disabled, or not E.164 | voice dial, SMS sends with an explicit `from`                       |
| `INVALID_EMAIL`          | The email recipient address is malformed                              | unified send, `POST /api/v1/messages/email`                         |
| `MISSING_REQUIRED_FIELD` | A field the endpoint requires is absent from the body                 | most write endpoints                                                |

<Note>
  None of these is retriable as-is. The gate runs pre-send — nothing is
  billed, nothing is dispatched — so re-sending the unchanged body is a
  guaranteed second refusal. Fix the value first; re-send once.
</Note>

## Decode the envelope

A validation refusal always carries the field-level hint in `error.details`
and a sentence in `error.message`. Read both before touching anything:

```json theme={null}
{
  "error": {
    "code": "INVALID_PHONE_NUMBER",
    "status": 422,
    "message": "The 'to' field must be a valid E.164 phone number",
    "details": { "field": "to", "value": "07123 456789" }
  },
  "meta": { "request_id": "req_3f9a1b2c", "timestamp": "2026-09-15T09:41:22Z" }
}
```

* `error.code` tells you which gate tripped.
* `error.message` names the field in prose.
* `error.details.field` and `error.details.value` (when present) echo the
  exact value your integration sent — the Delivery Log records the refused
  body too, so you can compare against what you intended.
* `meta.request_id` is what support needs if the value looks right to you
  but still refuses.

## The canonical shape: E.164 and friends

Every phone gate on the platform enforces the same format: **E.164 — a
`+` followed by 2–15 digits, with a non-zero country code**. The frequent
rejections, in order of how often they appear:

| You sent               | Why it refuses                       | The corrected value        |
| ---------------------- | ------------------------------------ | -------------------------- |
| `07123 456789`         | national format, no country code     | `+447123456789`            |
| `+44 (0) 7123 456789`  | trunk `0` and punctuation            | `+447123456789`            |
| `00447123456789`       | international prefix `00`, not `+`   | `+447123456789`            |
| `+123`                 | far too short for any numbering plan | the full subscriber number |
| `+1234567890123456789` | over 15 digits                       | the full subscriber number |

Email gates enforce a simpler shape: a non-empty local part, one `@`, and a
domain with at least one dot. `NOT AN EMAIL`, `" "` (whitespace-only), and
`user@` all refuse with `INVALID_EMAIL`; `user@example.com` passes.

## Pre-flight the value

You never need to learn a recipient is invalid from a refused send — two
checks run before you send anything.

### Number lookup

`GET /api/v1/numbers/lookup/{phoneNumber}` resolves a number's line type,
carrier, and validity. It is the authoritative answer for "will this value
pass?": a `valid: false` response means the E.164 gate refuses that value
on every send surface, and the national-format echo shows the normalized
form to store. URL-encode the leading `+` as `%2B`:

```bash theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/numbers/lookup/%2B447123456789" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The full field set (line type, portability, SIM swap, reachability packages)
is on [Number Lookup](/numbers/lookup); the data model behind it is
[Line type and reachability](/concepts/line-type-and-reachability).

### E.164 formatting at collection time

Normalize to E.164 the moment a number enters your system — at form
collect, CSV import, or webhook intake — and store only the normalized
form. Any libphonenumber port does the parse; pass the region the user
typed the number in:

```typescript Node.js (libphonenumber-js) theme={null}
import { parsePhoneNumberFromString } from "libphonenumber-js";

function toE164(input, region = "GB") {
  const parsed = parsePhoneNumberFromString(input, region);
  return parsed && parsed.isValid() ? parsed.number : null; // null → refuse client-side
}
```

```python Python (phonenumbers) theme={null}
import phonenumbers  # the phonenumbers package — libphonenumber port

def to_e164(input_: str, region: str = "GB"):
    try:
        parsed = phonenumbers.parse(input_, region)
    except phonenumbers.NumberParseException:
        return None  # refuse client-side
    if not phonenumbers.is_valid_number(parsed):
        return None
    return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
```

```bash curl (normalize via lookup when a stored value can't be trusted) theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/numbers/lookup/07123456789?region=GB" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

### The `from_number` source-id resolution post-check

`INVALID_FROM_NUMBER` is not a format gate — it is a **source gate**. The
`from` you passed did not resolve to an active, dial- or send-capable
source on your org: a caller-ID id that was never verified, a numeric
sender that was released or disabled, or a field left empty where the
endpoint needs one. The post-check pattern is: read the exact `from` out
of the refused body, confirm it exists and is active for your org, then
re-resolve — never the same value blind.

1. List what your org can actually send from, and pick from that list;
   the softphone caller-ID dropdown and **Make a Call** dialog pre-filter
   to selectable, dial-ready sources.
2. For platform numbers, confirm the row shows the capability you need
   (`voice` for calls, `sms` for messages) on **Settings → Numbers**.
3. For an external caller ID, complete the register → challenge → confirm
   flow once (`POST /api/v1/voice/caller-ids/verify`) — the confirmed id is
   then selectable like an owned number.

When no `from` is usable at all, voice dial fails closed with a
`INVALID_FROM_NUMBER` reading *"No caller ID available"* — buy a
voice-capable number or set a default caller ID before dialing. That
fail-closed behaviour is deliberate: guessing a source would route from
the wrong identity.

## The fix, per code

Every fix follows the same shape: find the rejected body, correct the one
field the envelope names, re-send once.

### `INVALID_PHONE_NUMBER` — recipient format

Rejected body:

```json theme={null}
{
  "to": "07123 456789",
  "from": "+15550101010",
  "body": "Your appointment is tomorrow at 10am."
}
```

The `to` is a UK national-format number with spaces — it fails the E.164
gate (`+` plus 2–15 digits). Corrected body, which is accepted:

```json theme={null}
{
  "to": "+447123456789",
  "from": "+15550101010",
  "body": "Your appointment is tomorrow at 10am."
}
```

The same fix applies wherever a phone field is refused: `POST
/api/v1/messages/sms` and the per-channel send variants, voice dial
endpoints, `POST /api/v1/voice/caller-ids/verify`, and flow send nodes
(`to must be a valid E.164 phone number`).

### `INVALID_FROM_NUMBER` — the source you sent from

Rejected body — the numeric source passed in `from` is not an active
source on your org:

```json theme={null}
{
  "to": "+14155550100",
  "from": "+15550109999",
  "body": "Your code is 4412."
}
```

Corrected body — re-resolved to an active sender your org actually owns
(or a verified caller-ID id):

```json theme={null}
{
  "to": "+14155550100",
  "from": "+15550101010",
  "body": "Your code is 4412."
}
```

### `INVALID_EMAIL` — email recipient format

The unified-send `422` naming a malformed recipient:

```json theme={null}
{
  "error": {
    "code": "INVALID_EMAIL",
    "status": 422,
    "message": "The email recipient is malformed.",
    "details": { "field": "to_email", "value": "noreply@ " }
  }
}
```

`"noreply@ "` (a trailing whitespace, no domain) refused; `"noreply@example.com"`
passes. Trim whitespace and refuse empties in your own form validation so
the bad value never reaches the API.

### `MISSING_REQUIRED_FIELD` — an absent field

The endpoint's schema requires a field your body omitted. The message names
the field; the refusal is `400` or `422` depending on surface:

```json theme={null}
{
  "error": {
    "code": "MISSING_REQUIRED_FIELD",
    "status": 400,
    "message": "Missing required field: 'body'",
    "details": { "field": "body" }
  }
}
```

Add the named field and re-send. This class includes schema-level
`VALIDATION_ERROR` siblings whose `details.issues` array lists every
offending field at once — fix all of them in one edit, then a single
re-send succeeds.

## What not to do

* **Do not retry an unchanged body.** Every gate above is deterministic;
  the second attempt returns the byte-identical refusal and burns your
  rate-limit budget on a decision only you can change.
* **Do not store national-format numbers and normalize at send time.**
  Normalize at collection, store only E.164, and send the stored form —
  a send-time formatter is one more thing that can disagree with the gate.
* **Do not hand-roll a regex as your only E.164 validator.** The gate
  checks country-code and numbering-plan validity, not just shape; a
  libphonenumber port (or the lookup endpoint for stored values you cannot
  trust) matches the gate. A regex accepts `+999999999` — the gate does not.
* **Do not "fix" `INVALID_FROM_NUMBER` by trying other values in a loop.**
  Pick from the selectable list of sources the dashboard already filters
  for you; a loop of guesses is a rate-limited way to learn the same thing.

## Paste this into a support ticket

When the envelope's `details.value` looks correct to you but the refusal
persists — or lookup returns `valid: false` for a number you believe is
assigned — open a ticket with:

```
Error code: <e.g. INVALID_PHONE_NUMBER>
Endpoint: <e.g. POST /api/v1/messages/sms>
request_id: <from error.meta.request_id>
Field and value refused: <e.g. to = "+447123456789">
Lookup result for the same value: <valid: true|false, line type>
What you changed before the last attempt: <e.g. normalized at import>
```

## Where these codes link

* [Error Code Reference — Validation family](/reference/error-codes#validation) —
  the full catalog where these four codes sit, with HTTP status per code.
* [FAQ — "Why did my API call reject a phone number with an E.164 validation error?"](/reference/faq#why-did-my-api-call-reject-a-phone-number-with-an-e164-validation-error) —
  the short-form summary that hands off here.
* [Number Lookup](/numbers/lookup) — the pre-flight endpoint's field set.
* [E.164 and the send surfaces](/reference/faq#what-phone-number-format-does-orbit-use) —
  the shared format answer on the FAQ.
* [Troubleshooting hub](/reference/troubleshooting-hub) — every other runbook,
  grouped by surface.
