ReachBellDocs

Error codes

Every non-2xx response from the ReachBell API uses the same JSON envelope. Build your error handling against that shape once and every endpoint behaves the same.

Error response shape

{
  "statusCode": 404,
  "message":    "Project not found",
  "requestId":  "req_01HX4..."
}
FieldMeaning
statusCodeHTTP status. Always matches the response's HTTP status line.
messageHuman-readable explanation. Safe to log; not necessarily safe to surface to end users.
requestIdUnique ID for the request. Searchable in your API logs and in Sentry. Quote it when you contact support.

Always log the requestId. It's the single most useful piece of information when something goes wrong — it lets us trace your exact request through our infrastructure in seconds.

HTTP status codes

StatusWhat it means at ReachBell
400 Bad RequestThe request was malformed — invalid JSON, missing required field at the protocol level.
401 UnauthorizedYour JWT is missing, expired, or your x-api-key is unknown. Re-authenticate.
403 ForbiddenYou are authenticated, but you don't have permission. Usually one of: VIEWER role attempting a write, or you tried to read a resource that belongs to a different organization or project.
404 Not FoundThe resource doesn't exist, or it does exist but it belongs to a different org/project than the one your credentials are scoped to. We don't leak existence across tenants.
409 ConflictA duplicate or contradictory operation. The most common case is registering a subscriber whose push token already exists, or editing a campaign that's already sending.
422 Unprocessable EntityDTO validation failed — a field is the wrong type, missing, or out of range. The message will list the failing field.
429 Too Many RequestsYou're past the rate limit. Read the Retry-After header (seconds) and back off.
500 Internal Server ErrorA bug on our side. Send us the requestId.
502/503/504Transient infrastructure issue. Retry with exponential backoff.

422 validation details

422 responses include a details array with one entry per failing field:

{
  "statusCode": 422,
  "message":    "Validation failed",
  "requestId":  "req_01HX4...",
  "details": [
    { "field": "content.title", "error": "must be a string" },
    { "field": "schedule.sendAt", "error": "must be a valid ISO 8601 date" }
  ]
}

429 backoff

curl -i https://api.reachbell.com/transactional/send -H "x-api-key: ..."
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "statusCode": 429,
  "message":    "Rate limit exceeded",
  "requestId":  "req_01HX4..."
}

Sleep Retry-After seconds, then retry once. If you hit 429 again, double the wait and retry up to three times before surfacing the error.

Route-specific reasons

Some endpoints return 200 OK with a reason field rather than a non-2xx — most notably the transactional endpoints, where "not delivered" is a normal business outcome.

POST /transactional/send

reasonMeaning
subscriber_not_foundNo subscriber matched the externalId or token.
subscriber_unsubscribedThe user has revoked browser permission. The SDK will not re-prompt unless they reset it manually.
subscriber_inactiveSubscriber is in a sandbox project but credentials are live, or vice versa.
frequency_cappedThe project's delivery policy blocks another push within the cap window.
quiet_hoursQuiet hours are active in the subscriber's local timezone.
provider_errorFCM / APNs / VAPID rejected. Check the dashboard for provider-level diagnostics.
payload_too_largePush payload exceeded the 4 KB FCM limit — usually a too-large image URL or too many actions.

POST /transactional/email

reasonMeaning
sender_domain_not_verifiedsenderEmail is not on a verified domain for the project.
recipient_bouncedThe address previously bounced and is suppressed. Remove from your list.
recipient_complainedThe address marked previous mail as spam. Permanently suppressed.
template_not_foundThe templateId doesn't exist on this project.
template_variables_missingA {{variable}} in the template was not supplied in variables.

POST /transactional/whatsapp

reasonMeaning
template_not_approvedThe named template isn't approved by Meta yet.
outside_service_windowMore than 24 hours since the user last messaged you and no template provided.
recipient_opted_outThe user blocked your business number.

POST /transactional/sms

reasonMeaning
invalid_phoneThe number failed E.164 validation.
carrier_rejectedThe carrier refused delivery — usually content filtering or destination-country rules.
unverified_sender_idThe senderId is not provisioned for the destination country.

A worked example

A typical client-side handler looks like this:

const res = await fetch('https://api.reachbell.com/campaigns', {
  method:  'POST',
  headers: {
    'x-api-key':    apiKey,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(payload),
});

if (res.status === 429) {
  const wait = Number(res.headers.get('Retry-After') ?? 5);
  await new Promise(r => setTimeout(r, wait * 1000));
  return retry();
}

if (!res.ok) {
  const err = await res.json();
  console.error('[reachbell]', err.requestId, err.message);
  throw new Error(err.message);
}

return res.json();

The requestId log line is what saves you when you reach out to support.

What's next?

  • Tune your rate limit handling.
  • Re-read the Transactional API for the reason codes in context.
  • Set up structured logging so requestId is always captured alongside your business request ID.