OORQO API

Errors

One error envelope for every failure, stable codes to branch on, and when it is safe to retry.

Every failure from /api/v1 has the same shape, whatever produced it:

{
  "error": {
    "code": "validation_failed",
    "message": "The email field must be a valid email address.",
    "fields": {
      "email": ["The email field must be a valid email address."]
    }
  }
}
FieldAlwaysMeaning
error.codeyesStable, lowercase snake_case. The contract.
error.messageyesA sentence for a developer reading a log. Wording changes without notice.
error.fieldsnovalidation_failed only: messages per field, keyed by field name (template.name for nested fields).
error.detailsnoExtra context for the code, such as state or limit.

Branch on code, never on message

const res = await fetch(url, options);
if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case 'whatsapp_window_closed':
      return sendTemplateInstead();
    case 'plan_limit_reached':
      return notifyBilling(error.details);
    case 'rate_limited':
      return retryAfter(res.headers.get('Retry-After'));
    default:
      // A code this build predates: fall back to the HTTP status.
      return res.status >= 500 ? scheduleRetry() : failPermanently(error);
  }
}

New codes are added as features ship, so always keep a default branch.

Codes

StatusCodeWhenRetry?
400bad_requestThe request could not be read.No
400invalid_idempotency_keyIdempotency-Key is empty, too long or not printable ASCII.No
401unauthenticatedMissing, wrong, revoked or expired token.No
402subscription_requiredThe organization has no active plan. details.state says why.No
402plan_limit_reachedA plan limit is used up. details: key, limit, current.No
402organization_suspended_for_plan, channel_suspended_for_planThe organization or the channel is over its plan.No
403forbiddenThe token may not do this.No
403organization_requiredThe token's organization is no longer available.No
404not_foundNo such resource in your organization, or no such path.No
404contact_not_found, whatsapp_channel_not_foundcontact_id / channel_account_id of a message is not yours.No
405method_not_allowedWrong HTTP method for the path.No
409invalid_stateThe resource's status does not allow the action. details.status is the current one.No
409contact_already_existsPATCH /contacts/{id}: the new phone number belongs to a deleted contact (a live contact with it is a 422).No
409idempotency_key_reusedThe key was used with a different body.No
409idempotency_in_progressThe first request with this key is still running.Yes, shortly
422validation_failedA field or query parameter is invalid. See fields.No
422launch_rejectedThe campaign cannot be launched yet (WhatsApp not ready, no recipients…).After fixing
422whatsapp_window_closedThe 24-hour service window is closed: send an approved template.No
422whatsapp_template_not_approvedThe template is not approved for this number and language.No
422whatsapp_channel_unavailableNo active WhatsApp number to send from.After fixing
422contact_phone_requiredThe contact has no phone number.No
429rate_limitedToo many requests. Wait Retry-After seconds.Yes
500server_errorSomething broke on our side.Yes, with backoff
502whatsapp_provider_error, whatsapp_media_upload_failedWhatsApp refused the message. details has the provider's code.Depends on details
503service_unavailableTemporarily unavailable.Yes, with backoff

The same list is published as JSON at /errors.json.

Retrying safely

  • Retry only 409 idempotency_in_progress, 429, and 5xx. Every other 4xx fails the same way forever.
  • Back off exponentially (for example 1 s, 2 s, 4 s … capped at 60 s) and honour Retry-After.
  • Send an Idempotency-Key with every creating request so a retry after a timeout cannot create or send twice.

A 502 on a message is not always a failure to send

whatsapp_provider_error means WhatsApp answered with an error, and the message is stored as failed (details.message_id). Retrying with the same Idempotency-Key is safe: a definitively failed attempt may be retried, while an accepted one is returned instead of being sent again.

On this page