R2 Developers
R2 Connect API

Errors

Error shape, what each code means and how to react to each one.

Errors use standard HTTP status codes and always carry the same JSON body, with a stable code for your logic and a human-readable message for your logs.

json
{
  "error": {
    "code": "forbidden",
    "message": "Esta API key no tiene el permiso 'read:payments'."
  }
}

Codes#

HTTPCodeWhat happenedWhat to do
400bad_requestA parameter has an invalid format, almost always a date.Fix the parameter. Retrying as-is will not help.
401unauthorizedThe key is missing, invalid or was revoked.Stop syncing and ask the owner for a new key.
403forbiddenThe key is valid but lacks that resource’s scope.Ask the owner for a key with the scope, or stop querying that resource.
404not_foundThe resource does not exist in that account, or the path is misspelled.Check the order number and the path.
429rate_limitedYou exceeded the request limit.Wait as long as Retry-After says and retry.
500internal_errorA failure on R2’s side.Retry with growing backoff. If it persists, report it with the X-Request-Id.

Isolation between businesses

If you request a record that exists but belongs to another business, the response is 404, not 403. This is deliberate: the API does not reveal even the existence of data outside your key.

javascript
const res = await fetch(url, { headers });

if (!res.ok) {
  const { error } = await res.json();

  switch (error.code) {
    case "unauthorized":   // dead key: retrying is pointless
      await notifyCustomer("Please reconnect your R2 account");
      return;
    case "forbidden":      // missing scope: stop requesting this resource
      disableResource(url);
      return;
    case "rate_limited":   // wait and retry
      return retryAfter(res.headers.get("Retry-After"));
    default:
      log(error.code, error.message, res.headers.get("X-Request-Id"));
  }
}