Skip to main content
Every Robase error is a single JSON shape:
{
  "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

The request body is malformed or missing a required field. field tells you which one. Fix: correct the payload and retry.
{ "type": "validation_error", "message": "recipient required", "field": "to" }
SMS only. The to field is not a valid E.164 phone number. Must start with + and contain 8–15 digits.
{ "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 is the standard tool.
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).
{ "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 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.
The API key is missing, malformed, revoked, or unknown.Fix: double-check the Authorization: Bearer ... header. Rotate the key if needed.
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.
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.
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.
Same Idempotency-Key used with a different request body within 24 hours.Fix: use a new idempotency key, or match the original body exactly.
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.
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.
Specific to SMS: monthly SMS segment quota used up, wallet empty. Same fix as quota_exceeded.
Email only. You tried to send from a domain that isn’t DNS-verified yet.Fix: complete DKIM + SPF + DMARC setup. See Domains.
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.
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.
Something went wrong on our side. Our on-call will page.Fix: retry with exponential backoff. If it persists, check status.robase.dev.

Pattern: a handler that covers all cases

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;
  }
}