ReluAutomate
Core concepts

API errors and retries

Decide which failed API requests to retry, including planned maintenance during Relu releases.

Use the HTTP status and response body to decide whether to retry a failed API request. This page covers requests that Relu did not accept successfully. If an order was accepted and later reached job_status: "Failed", see Order processing errors instead.

Decide whether to retry

Use this as the default policy unless an endpoint documents different behaviour:

ResultWhat to do
400, 401, 403, 404, 409, 422Fix the request, credentials, permissions, or conflicting state. Do not retry it unchanged.
408Retry only if the operation is safe to repeat.
429Wait for Retry-After when present, then retry.
500, 502, unmarked 503, 504Retry a few times if the operation is safe to repeat.
Network error or timeoutRetry a read. For a write, the outcome may be unknown, so check before sending it again.
503 with code: "maintenance"Wait for retry_after_seconds and retry. Do not apply the ordinary retry limit.

Do not retry every 4xx: most mean the same request will fail again. In particular, validation errors will keep returning 400 or 422 until you change the request.

Coded refusals

Some 403 and 429 responses carry a detail object with a code and a message, so your integration can branch without parsing text:

codeStatusMeaningRetry
payment_verification_required403The organization that would own or owns the order must add a payment method before it can create or submit orders.After that organization adds a payment method.
submission_rate_limited429The organization reached its hourly submission limit.Later.
account_blocked403The organization is blocked. Contact support.No.
order_owner_refused403You reached the order through an invitation, a share link or an embedded viewer, and its owner cannot have it opened or submitted right now.No. Only the owner can resolve it.

order_owner_refused uses one code and one message for every cause, so it does not say what is wrong with the owner. The same request by the same guest keeps failing until the owner's situation changes. Show the message and point the user to whoever provided the order.

Backoff and jitter

For ordinary temporary failures, make at most five attempts. Add a random delay before each retry, increasing the maximum delay after every failure:

RetryWait up to
11 second
22 seconds
34 seconds
48 seconds

Choosing a random value between zero and the maximum adds jitter. It prevents many clients from retrying simultaneously when the API recovers. Cap longer retry sequences at 30 seconds between attempts.

Planned maintenance

During a Relu release, the API may return 503 Service Unavailable with this response body:

{
  "code": "maintenance",
  "retry_after_seconds": 30
}

This marked response is different from an ordinary 503. It is generated before the request reaches the application, so the request was not processed and is safe to retry, including for writes such as POST /orders.

When you receive it:

  1. Wait for retry_after_seconds, or 30 seconds if the field is absent.
  2. Retry the same request.
  3. Keep doing this while the marked maintenance response continues, or until your application cancels the operation.

Maintenance retries do not count towards the normal retry limit. If maintenance lasts longer than expected, check the Relu status page for updates.

Only apply this behaviour when both the status is 503 and the body contains "code": "maintenance". Treat an unmarked 503 as an ordinary temporary failure.

Retry example

The examples below apply this policy to a GET. A marked maintenance response waits and retries without consuming one of the five ordinary attempts.

import random
import time

import requests


def get_from_relu(path):
    failures = 0

    while True:
        try:
            response = requests.get(
                f"https://api.automate.relu.ai{path}",
                headers={"X-API-Key": "<your_api_token>"},
                timeout=60,
            )
        except (requests.ConnectionError, requests.Timeout):
            if failures == 4:
                raise
            time.sleep(random.uniform(0, min(30, 2**failures)))
            failures += 1
            continue

        if response.status_code == 503:
            try:
                body = response.json()
            except ValueError:
                body = {}
            if body.get("code") == "maintenance":
                time.sleep(body.get("retry_after_seconds", 30))
                continue

        if response.ok:
            return response.json()

        if response.status_code not in {408, 429, 500, 502, 503, 504}:
            response.raise_for_status()
        if failures == 4:
            response.raise_for_status()

        retry_after = response.headers.get("Retry-After")
        delay = (
            int(retry_after)
            if retry_after and retry_after.isdigit()
            else random.uniform(0, min(30, 2**failures))
        )
        time.sleep(delay)
        failures += 1
const sleep = (milliseconds) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

async function getFromRelu(path) {
  let failures = 0;

  while (true) {
    let response;
    try {
      response = await fetch(`https://api.automate.relu.ai${path}`, {
        headers: { 'X-API-Key': process.env.RELU_API_TOKEN },
        signal: AbortSignal.timeout(60_000),
      });
    } catch (error) {
      if (failures === 4) throw error;
      await sleep(Math.random() * Math.min(30, 2 ** failures) * 1000);
      failures += 1;
      continue;
    }

    if (response.status === 503) {
      const body = await response
        .clone()
        .json()
        .catch(() => ({}));
      if (body.code === 'maintenance') {
        await sleep((body.retry_after_seconds ?? 30) * 1000);
        continue;
      }
    }

    if (response.ok) return response.json();

    const retryable = [408, 429, 500, 502, 503, 504];
    if (!retryable.includes(response.status) || failures === 4) {
      throw new Error(`Relu API returned HTTP ${response.status}`);
    }

    const retryAfterHeader = response.headers.get('Retry-After');
    const retryAfter =
      retryAfterHeader === null ? NaN : Number(retryAfterHeader);
    const delaySeconds =
      Number.isFinite(retryAfter) && retryAfter >= 0
        ? retryAfter
        : Math.random() * Math.min(30, 2 ** failures);
    await sleep(delaySeconds * 1000);
    failures += 1;
  }
}

Retrying writes

Reads such as GET /services and GET /orders/{id} are safe to repeat. Be more careful with requests that create or change data.

A timeout, network error, or ordinary 5xx can happen after the server has accepted a write but before you receive its response. Automatically repeating that request can create duplicate work. Retry a write only when the endpoint documents that it is idempotent, or when you receive the explicit maintenance response above, which means the request was not processed.

Troubleshooting persistent failures

For repeated 5xx responses, check Service status. If you contact Relu support, include the HTTP status, request method and path, timestamp, X-Request-ID response header when present, and order_id when applicable. Never send your API key.

On this page