Reference

Error Handling

All error responses follow a single JSON structure. Use HTTP status codes to route your error handling logic, and the error field for human-readable context.

Error response format
json
// Every non-2xx response returns:
{
  "error": "human-readable description of what went wrong"
}

Error codes

The table below covers all status codes returned by the Connect Hub. The Retry column indicates whether automatic retry is safe.

CodeNameRetryDescription
400Bad RequestNOMissing or invalid fields (e.g. phone not E.164, missing required body parameter). Fix your payload.
401UnauthorizedNOMissing or invalid x-api-key header. Check your API key — do not retry automatically.
402Payment RequiredNOInsufficient account balance. Top up your Novauth account before retrying.
403ForbiddenNOThe API key does not have permission to perform this action.
404Not FoundNOResource does not exist (e.g. unknown UUID on /call/flash/{uuid}/cdr). Do not retry.
409ConflictNOA duplicate or conflicting operation was detected (e.g. reporting webhook twice for same UUID).
422Unprocessable EntityNOTelegram-specific: request semantically invalid (e.g. expired request_id on check-verification-status).
429Too Many RequestsYESRate limit exceeded. Honour the Retry-After header and implement exponential backoff.
480Temporarily UnavailableYESCallee temporarily unreachable (SIP-derived). Safe to retry after a short delay.
482Loop DetectedNORequest routing loop detected. Do not retry — investigate your integration.
486Busy HereYESCallee is busy. Retry after a delay or fall back to another channel.
488Not Acceptable HereNORequest parameters not acceptable (e.g. unsupported codec or format). Fix the request.
500Internal Server ErrorYESUnexpected server-side failure. Retry with exponential backoff (max 3 attempts).
502Bad GatewayYESUpstream service (carrier, gateway) returned an invalid response. Retry after a brief wait.
503Service UnavailableYESService temporarily overloaded or under maintenance. Honour Retry-After if present.
603DeclineNOCallee explicitly declined (SIP-derived). Do not retry — user actively rejected the call.

Retry logic

Only retry on codes 429, 480, 486, 500, 502, 503. Use exponential backoff starting at 500 ms (double each attempt). Never auto-retry 4xx client errors — fix the root cause first.

javascript
async function callWithRetry(fn, { maxAttempts = 3, baseDelay = 500 } = {}) {
  // Safe codes to retry: 429, 500, 502, 503
  const RETRYABLE = new Set([429, 500, 502, 503]);

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fn();

    if (res.ok) return res;

    const { status } = res;

    // Non-retryable — surface error immediately
    if (!RETRYABLE.has(status)) {
      const body = await res.json().catch(() => ({}));
      throw Object.assign(new Error(body.error ?? 'API error'), { status, body });
    }

    if (attempt === maxAttempts) {
      throw Object.assign(new Error('Max retries reached'), { status });
    }

    // Exponential backoff: 500ms, 1000ms, 2000ms …
    const delay = baseDelay * 2 ** (attempt - 1);
    await new Promise(r => setTimeout(r, delay));
  }
}

// Usage
const res = await callWithRetry(() =>
  fetch('https://api.novauth.com/api/v1/connect-hub/call/flash', {
    method: 'POST',
    headers: {
      'x-api-key':    process.env.BETATEL_API_KEY,
      'x-account-id': process.env.BETATEL_ACCOUNT_ID,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ callee: '+14155552671' })
  })
);

Idempotent operations (GET status checks) can be retried freely. For POST operations that create resources, always check whether the resource was already created before retrying — use the returned UUID to query status first.

Fallback cascade

Verify Call works in most regions but is not universal. Implement a cascade so users always receive their OTP even when the primary channel fails.

Recommended fallback order
Verify Call
fastest
Telegram OTP
if user has TG
SMS
universal
Trigger conditions: Fall back when the primary channel returns a non-retryable error (400, 402, 488, 603) or when the verification session is not reported as Success within a configurable timeout window (recommended: 15 s for Verify Call).
javascript
async function sendWithFallback(phone, otp) {
  // 1. Try Verify Call first
  try {
    const flash = await fetch('.../call/flash', {
      method: 'POST',
      headers,
      body: JSON.stringify({ callee: phone })
    });
    if (flash.ok) return { channel: 'flash', ...(await flash.json()) };
  } catch {}

  // 2. Flash failed — fall back to Telegram if user has it
  try {
    const tg = await fetch('.../telegram', {
      method: 'POST',
      headers,
      body: JSON.stringify({ to: phone, code: otp, ttl: 120 })
    });
    if (tg.ok) return { channel: 'telegram', ...(await tg.json()) };
  } catch {}

  // 3. Final fallback: SMS (widest global reach)
  const sms = await fetch('.../sms', {
    method: 'POST',
    headers,
    body: JSON.stringify({ to: phone, text: `Your code: ${otp}` })
  });
  if (!sms.ok) throw new Error('All channels failed');
  return { channel: 'sms', ...(await sms.json()) };
}

To disable the cascade for a specific flow, simply omit fallback try/catch blocks and surface the error directly to the user. There is no server-side cascade setting — the logic lives entirely in your integration code.

Rate limiting

Rate limits are enforced at the API gateway layer. When a limit is hit you receive a 429 Too Many Requests response. Check the Retry-After header for the exact wait time.

Retry-After
Seconds to wait before the next request is allowed.
X-RateLimit-Limit
Maximum requests allowed in the current window.
X-RateLimit-Remaining
Requests remaining in the current window.
X-RateLimit-Reset
Unix epoch when the current window resets.
javascript
const res = await fetch(url, { method: 'POST', headers, body });

if (res.status === 429) {
  const retryAfter = Number(res.headers.get('Retry-After') ?? 1);
  console.warn(`Rate limited — waiting ${retryAfter}s`);
  await new Promise(r => setTimeout(r, retryAfter * 1000));
  // retry the request
}

Common integration mistakes

These are the most frequent issues reported during integration. Check this list before opening a support ticket.

1
Sending phone numbers without country code
Use E.164 format. "+14155552671" not "4155552671" or "014155552671".
2
Calling the API from the browser
Your API key will be exposed in client-side code. Always proxy through your backend server.
3
Not handling 402 Payment Required
Balance runs out silently in production. Monitor your Novauth account balance and set up alerts.
4
Retrying 400 and 401 errors
These are client errors — retrying wastes budget and adds latency. Fix the root cause first.
5
Ignoring the Telegram TTL
Codes expire after the ttl you specify. If you call check-verification-status after expiry you will get a 422.
6
Parsing the Telegram Gateway body before verifying the signature
Always verify the HMAC on the raw bytes before JSON.parse(). Re-serialization changes byte order.
7
Not revoking Telegram codes after success
Call POST /revoke-verification after a successful code_valid response to prevent replay attacks.
8
Using Verify Call as the only channel without a fallback
Verify Call fails in some networks. Always implement a fallback to SMS or Telegram for critical flows.