Errors
The response envelope, top-level error codes, and field-level validation details.
Every VeilBrowser API returns the same envelope, so a single client-side handler covers both the cloud API and workers.
Response envelope
Successful responses carry the payload under data:
{
"success": true,
"data": { "id": "prof_01j...", "name": "marketing-us" }
}Failures carry a structured error instead:
{
"success": false,
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Profile not found: prof_01j..."
}
}success is a literal discriminant, so narrowing on it is safe in TypeScript.
Error codes
error.code is one of eight values. Branch on this, not on the HTTP status or the
message text.
Prop
Type
One code, several statuses
A few codes map to more than one HTTP status because the same class of problem can
surface at different layers. RESOURCE_NOT_FOUND is 404 for a missing profile but
400 when a launch requests a browser version the worker does not have installed —
the request itself is malformed in that case.
Validation details
VALIDATION_ERROR responses include a details array with one entry per offending
field, so you can map failures straight onto a form.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "browserVersion",
"code": "TOO_SMALL",
"message": "Browser version must be at least 135",
"received": 131,
"constraint": { "min": 135, "max": 150 }
}
]
}
}Prop
Type
Field-level code values: INVALID_TYPE, INVALID_FORMAT, TOO_SHORT, TOO_LONG,
INVALID_PATTERN, TOO_SMALL, TOO_BIG, NOT_INTEGER, ARRAY_TOO_SHORT,
ARRAY_TOO_LONG, INVALID_ENUM_VALUE, INVALID_UNION, REQUIRED,
UNRECOGNIZED_KEY, CUSTOM.
Some errors enrich constraint with actionable data rather than plain bounds. A launch
against an uninstalled browser version returns the versions that host does have:
{
"field": "browserVersion",
"code": "RESOURCE_NOT_FOUND",
"message": "Browser version 149 not found",
"received": 149,
"constraint": { "availableVersions": [135, 141, 147] }
}Handling errors
type ApiError = {
success: false;
error: { code: string; message: string; details?: { field: string; message: string }[] };
};
const res = await fetch(`${base}/v1/profiles`, { method: "POST", body });
const body = await res.json();
if (!body.success) {
const { code, message, details } = (body as ApiError).error;
switch (code) {
case "VALIDATION_ERROR":
// Map details onto form fields
for (const d of details ?? []) markInvalid(d.field, d.message);
break;
case "AUTHORIZATION_ERROR":
// Plan limit or missing role — surface, do not retry
throw new Error(message);
case "EXTERNAL_SERVICE_ERROR":
case "INTERNAL_ERROR":
// Transient — safe to retry with backoff
return retry();
default:
throw new Error(message);
}
}Retry EXTERNAL_SERVICE_ERROR and INTERNAL_ERROR with exponential backoff. Never
retry VALIDATION_ERROR, AUTHENTICATION_ERROR or AUTHORIZATION_ERROR — the same
request will fail identically.
Launch authorization failures
License authorization is not part of this envelope. POST /v1/license/authorize returns
402 Payment Required with an explicit reason when a launch is refused:
{
"allowed": false,
"reason": "concurrency_limit",
"current": 200,
"limit": 200
}Workers fail closed on this response, and on any failure to reach the cloud. See Licensing & concurrency and Troubleshooting.