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#
| HTTP | Code | What happened | What to do |
|---|---|---|---|
400 | bad_request | A parameter has an invalid format, almost always a date. | Fix the parameter. Retrying as-is will not help. |
401 | unauthorized | The key is missing, invalid or was revoked. | Stop syncing and ask the owner for a new key. |
403 | forbidden | The key is valid but lacks that resource’s scope. | Ask the owner for a key with the scope, or stop querying that resource. |
404 | not_found | The resource does not exist in that account, or the path is misspelled. | Check the order number and the path. |
429 | rate_limited | You exceeded the request limit. | Wait as long as Retry-After says and retry. |
500 | internal_error | A 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.
Recommended handling#
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"));
}
}