Envelope
Every response, success or error, uses the same shape:
{
"success": false,
"code": 404,
"message": "Product not found.",
"error": { "code": "product.not_found" },
"data": null,
"_req": { "reqId": "019faa4c-6f6e-716a-a548-fc2ea447953b", "resTime": 6 }
}| Field | Meaning |
|---|---|
success | true for 2xx, false otherwise. |
code | Mirrors the HTTP status code. |
message | Human-readable description: safe to log, not always safe to show to end users. |
error | Machine-readable failure detail on error, null on success. See machine-readable error codes. |
data | Resource payload on success, null on error. |
_req | Request id and server-side timing. See request IDs. |
The HTTP status and the envelope code always agree. Branch on either.
Status codes
| Status | Meaning | When you'll see it |
|---|---|---|
200 | OK | Successful read or update. |
201 | Created | A resource was created. |
400 | Bad Request | Malformed body, or invalid query parameters such as limit=101. |
401 | Unauthorized | Missing, malformed, revoked, or non-member API key. Rotate the credential. |
402 | Payment Required | Your plan's limit is reached, or the resource is locked behind an upgrade. |
403 | Forbidden | The key is valid but lacks the required scope, project access, or product access. Do not rotate the key; widen its scopes. |
404 | Not Found | Resource doesn't exist, or your key can't see it. |
409 | Conflict | A slug or name is taken, a baseline drifted, or the action conflicts with current state. |
405 | Method Not Allowed | The path exists but does not serve this method. The Allow header lists the methods it does serve, and error.code is request.method_not_allowed. |
413 | Payload Too Large | The request body is larger than 1 MiB (1,048,576 bytes). error.code is request.body_too_large, and message states the limit. Split large batches by size as well as by count. |
429 | Too Many Requests | The API-key routes are not rate limited, so there a 429 only comes from infrastructure in front of the API, such as a CDN or a proxy. The anonymous exposed and example feeds allow 600 requests a minute per client and answer 429 request.rate_limited with a Retry-After header beyond that. Retry with backoff. |
500 | Internal Server Error | Unexpected. Retry with backoff, then check status.envless.cloud. |
503 | Service Unavailable | Planned maintenance. Every route except the maintenance status endpoint answers 503 while maintenance mode is on. Retry with backoff. |
Telling maintenance apart from a fault
During maintenance every route returns 503 with the message Service is currently under maintenance. Please try again later.. One endpoint stays up and needs no authentication, so retry logic can tell a planned window apart from a real fault instead of treating both as a 500:
curl https://api.envless.cloud/maintenance/status{
"success": true,
"message": "OK",
"data": { "maintenance": false }
}Back off and keep retrying on 503, rather than failing the job. A 500 on the other hand is unexpected and worth surfacing.
404 vs 403
The API returns 404, not 403, when a resource exists but your key can't see it. This avoids leaking the existence of projects, environments, or variables across workspaces.
If you're sure the resource exists and your key should reach it, double-check the workspace association in the dashboard.
Handling errors
const res = await fetch('https://api.envless.cloud/projects/my-app', {
headers: { Authorization: `Bearer ${token}` },
});
const body = await res.json();
if (!body.success) {
switch (body.code) {
case 401: throw new Error('Bad key, rotate it.');
case 404: throw new Error('Project not found.');
default: throw new Error(`Envless: ${body.message}`);
}
}res = requests.get(url, headers={"Authorization": f"Bearer {token}"}).json()
if not res["success"]:
if res["code"] == 401: raise RuntimeError("Bad key")
if res["code"] == 404: raise LookupError(res["message"])
raise RuntimeError(res["message"])headers = {"Authorization" => "Bearer #{token}"}
res = JSON.parse(Net::HTTP.get_response(URI(url), headers).body)
unless res["success"]
raise "Bad key" if res["code"] == 401
raise KeyError, res["message"] if res["code"] == 404
raise res["message"]
end$context = stream_context_create(['http' => [
'header' => 'Authorization: Bearer ' . $token,
'ignore_errors' => true,
]]);
$res = json_decode(file_get_contents($url, false, $context), true);
if (!$res['success']) {
throw match ($res['code']) {
401 => new RuntimeException('Bad key, rotate it.'),
404 => new OutOfBoundsException($res['message']),
default => new RuntimeException('Envless: ' . $res['message']),
};
}The SDKs do this unwrapping for you and raise a typed error carrying the same code, resource, field and request id: EnvlessApiError in TypeScript and in Python, Envless::ApiError in Ruby, and Envless\Exception\ApiException in PHP, where the code is errorCode.
Request IDs
Every response includes a _req field with the request ID and server-side timing:
{
"success": true,
"code": 200,
"message": "OK",
"error": null,
"data": { /* ... */ },
"_req": { "reqId": "019faa4c-6f6e-716a-a548-fc2ea447953b", "resTime": 14 }
}The same id is returned in the Request-Id response header. Send your own Request-Id header and it will be echoed back, so you can correlate a call with your own logs. Include reqId when you contact support; it points us at the exact request in ours.
Machine-readable error codes
On failure the envelope carries an error object. Branch on error.code, never on message; messages are prose and will change.
{
"success": false,
"code": 403,
"message": "auth.scope_missing: this key needs \"variables:write\".",
"error": { "code": "auth.scope_missing", "resource": "variables:write" },
"data": null,
"_req": { "reqId": "019faa4c-6f6e-716a-a548-fc2ea447953b", "resTime": 2 }
}error.resource names the scope or resource involved, and error.field names the offending request field when the failure is a validation error. Both keys are omitted when they don't apply, so only error.code is guaranteed. Every failure response carries a code; the full enumeration lives in the OpenAPI spec under components.schemas.ApiError.
| Prefix | Meaning |
|---|---|
auth.* | Credential, scope, or access problem. |
request.* | Something about the request itself: body, pagination, or a generic not-found or conflict when no resource-specific code applies. |
plan.* | A plan limit or upgrade wall. plan.upgrade_required is a 402 an upgrade fixes (a tier limit, a locked project, or version history and rollback on Free, where error.resource is version); plan.limit_reached is a 409 at the 1,000-variables-per-environment fair-use cap, which no plan raises. See Plans and billing. |
workspace.* | Workspace lookup, slug, limit, and deletion failures such as workspace.delete_has_projects. |
domain.* | Custom domain failures such as domain.not_configured or domain.attempt_limit. |
product.*, project.*, environment.*, variable.*, version.* | Resource-specific failures such as project.ambiguous_product or variable.baseline_drifted. |
member.*, invite.*, role.*, key.*, webhook.* | Team and integration failures. |
server.* | An unexpected failure on our side. |