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 carries a different payload but the same envelope.
POST /v1/license/authorize returns 402 Payment Required with an explicit reason
when a launch is refused, wrapped in the standard { success, data } shape:
{
"success": true,
"data": {
"allowed": false,
"reason": "concurrency_limit",
"current": 200,
"limit": 200
}
}success reports that the authorization check itself completed, not that the launch was
permitted. Read data.allowed for the verdict. A refusal the check never reached, such
as a bad API key, comes back as an ordinary { success: false, error } body.
Workers fail closed on this response, and on any failure to reach the cloud. See Licensing & concurrency and Troubleshooting.
Next steps
- Limits — the plan ceilings behind
AUTHORIZATION_ERROR. - API reference — the endpoints these codes come back from.