VeilBrowserDocs

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:

200 OK
{
  "success": true,
  "data": { "id": "prof_01j...", "name": "marketing-us" }
}

Failures carry a structured error instead:

404 Not Found
{
  "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.

400 Bad Request
{
  "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

client.ts
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:

402 Payment Required
{
  "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.

Next steps

  • Limits — the plan ceilings behind AUTHORIZATION_ERROR.
  • SDKgetEdenErrorMessage normalizes all of these to a string.
Was this page helpful?

On this page