> ## Documentation Index
> Fetch the complete documentation index at: https://docs.robase.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every error code we return, what causes it, and how to fix it.

Every Robase error is a single JSON shape:

```json theme={null}
{
  "error": {
    "type": "phone_invalid",
    "message": "phone number must be in E.164 format (e.g. +2348012345678)",
    "field": "to"
  }
}
```

* **`type`** — machine-readable error code, stable across versions. Switch on this.
* **`message`** — human-readable explanation. Never switch on this; we refine copy over time.
* **`field`** — when the error relates to a specific input field, this tells you which one.

The HTTP status code matches the severity — 4xx for client issues, 5xx for our problem.

## Error type reference

<AccordionGroup>
  <Accordion title="validation_error  ·  400">
    The request body is malformed or missing a required field. `field` tells you which one. Fix: correct the payload and retry.

    ```json theme={null}
    { "type": "validation_error", "message": "recipient required", "field": "to" }
    ```
  </Accordion>

  <Accordion title="phone_invalid  ·  400">
    SMS only. The `to` field is not a valid E.164 phone number. Must start with `+` and contain 8–15 digits.

    ```json theme={null}
    { "type": "phone_invalid", "message": "phone number must be in E.164 format (e.g. +2348012345678)", "field": "to" }
    ```

    Fix: normalize to E.164 before sending. [libphonenumber](https://github.com/google/libphonenumber) is the standard tool.
  </Accordion>

  <Accordion title="sender_id_invalid  ·  400">
    SMS only. Either the sender ID is longer than 11 characters, or it was **rejected** by carrier compliance (e.g. NCC rejected an NG sender).

    ```json theme={null}
    { "type": "sender_id_invalid", "message": "sender id was rejected by carrier compliance: impersonates a bank", "field": "from" }
    ```

    Fix: register a new compliant sender ID. See [Sender IDs](/sms/sender-ids).
  </Accordion>

  <Accordion title="dnd_blocked  ·  400">
    SMS only. The destination number is on the Do Not Disturb registry and this sender ID is not on the whitelist for that recipient.

    Fix: either register the sender for premium routes (contact support), or remove that recipient from your send list. See the [DND compliance guide](/guides/dnd-compliance).
  </Accordion>

  <Accordion title="authentication_error  ·  401">
    The API key is missing, malformed, revoked, or unknown.

    Fix: double-check the `Authorization: Bearer ...` header. Rotate the key if needed.
  </Accordion>

  <Accordion title="permission_error  ·  403">
    The key exists but lacks the permission for this endpoint (e.g. a `sending`-scoped key can't hit `GET /v1/sms`).

    Fix: use a `full`-permission key, or scope this operation to a different key.
  </Accordion>

  <Accordion title="not_found  ·  404">
    The resource you're fetching doesn't exist, or doesn't belong to your project.

    Fix: verify the ID. Remember that message IDs are UUIDs — not the provider-specific `provider_message_id`.
  </Accordion>

  <Accordion title="conflict  ·  409">
    The action conflicts with current state. E.g. you tried to cancel an SMS that's already `sent`.

    Fix: re-fetch the resource, check its state, only act if the state permits.
  </Accordion>

  <Accordion title="idempotency_conflict  ·  409">
    Same `Idempotency-Key` used with a different request body within 24 hours.

    Fix: use a new idempotency key, or match the original body exactly.
  </Accordion>

  <Accordion title="rate_limit_exceeded  ·  429">
    You're sending faster than the rate limit allows. The `Retry-After` header tells you when to try again.

    Fix: back off, add jitter. See [Rate limits](/concepts/rate-limits).
  </Accordion>

  <Accordion title="quota_exceeded  ·  402">
    Your monthly plan quota is used up AND your wallet has insufficient balance for overage.

    Fix: top up via **Dashboard → Billing**, or upgrade your plan.
  </Accordion>

  <Accordion title="sms_quota_exceeded  ·  402">
    Specific to SMS: monthly SMS segment quota used up, wallet empty. Same fix as `quota_exceeded`.
  </Accordion>

  <Accordion title="domain_unverified  ·  400">
    Email only. You tried to send from a domain that isn't DNS-verified yet.

    Fix: complete DKIM + SPF + DMARC setup. See [Domains](/email/domains).
  </Accordion>

  <Accordion title="address_suppressed  ·  400">
    Email only. The recipient is on your project's suppression list (bounce, complaint, manual add).

    Fix: remove from suppressions if you have consent to retry — but think twice about it.
  </Accordion>

  <Accordion title="upstream_error  ·  502">
    One of our upstream carriers is misbehaving. We've already retried internally; the message is in the DLQ.

    Fix: nothing on your end. Monitor the message's webhooks — we'll retry the upstream automatically.
  </Accordion>

  <Accordion title="internal_error  ·  500">
    Something went wrong on our side. Our on-call will page.

    Fix: retry with exponential backoff. If it persists, check [status.robase.dev](https://status.robase.dev).
  </Accordion>
</AccordionGroup>

## Pattern: a handler that covers all cases

```typescript theme={null}
import { Robase, RobaseError } from '@robase/node';

const pm = new Robase(process.env.ROBASE_API_KEY!);

try {
  const sms = await pm.sms.send({ to, from, body });
  return sms;
} catch (e) {
  if (!(e instanceof RobaseError)) throw e;

  switch (e.type) {
    case 'phone_invalid':
    case 'sender_id_invalid':
      // Permanent — don't retry, report to user.
      return { error: e.message };

    case 'rate_limit_exceeded':
    case 'upstream_error':
    case 'internal_error':
      // Transient — enqueue for retry.
      return enqueueRetry({ to, from, body });

    case 'sms_quota_exceeded':
    case 'quota_exceeded':
      await alertBillingOps(e.message);
      return { error: 'please top up your wallet' };

    default:
      throw e;
  }
}
```
