Skip to main content

Documentation Index

Fetch the complete documentation index at: https://productlane.mintlify.dev/docs/llms.txt

Use this file to discover all available pages before exploring further.

Every error response uses the same envelope:
{
  "error": {
    "code": "validation_failed",
    "message": "Validation failed: title is required.",
    "details": [{ "path": "title", "message": "Required" }],
    "request_id": "req_3727ca7b4f0fff3c"
  }
}
  • code - stable, machine-readable. Switch on this.
  • message - human-readable summary. Safe to surface to operators; not always safe to surface to end users verbatim.
  • details - optional, code-specific structure (validation paths, required scopes, rate-limit info).
  • request_id - also returned as the X-Request-Id header. Quote it in support tickets and we can pull the exact request from our logs.
Every response - success or error - carries X-Request-Id. Logging this header on the client side makes triage trivial.

Codes

HTTPCodeWhen
400validation_failedBody or query failed validation. details has the per-field reasons.
401unauthenticatedMissing or invalid bearer token.
403scope_requiredKey doesn’t have the scope this endpoint requires. details lists required and granted.
403forbiddenAuthenticated but blocked (plan gating, deleted resource, etc.).
404not_foundResource doesn’t exist or has been deleted.
409conflictSemantic conflict (e.g. closing an already-closed live chat).
410unsupported_key_versionYou passed a v1 key to a v2 endpoint.
422unprocessableWell-formed request we still can’t process (e.g. malformed cursor).
429rate_limitedToo many requests. See Rate limits.
500internal_errorOur problem. Retry with backoff; if it persists, share the request_id.

Details shapes

The details field varies by code. The shapes you’ll actually inspect:

validation_failed

Array of per-field reasons:
"details": [
  { "path": "title",   "message": "Required" },
  { "path": "tags.0",  "message": "Expected string, received number" }
]
path is a dotted path through the request body. Use it to highlight the offending field in your UI.

scope_required

"details": { "required": ["threads:write"], "granted": ["threads:read"] }
Tells you exactly which scope to add to the key.

rate_limited

"details": { "limit": 60, "window": "1m" }
The response also includes a Retry-After header - prefer that for retry timing.

Handling errors

type ApiError = {
  error: {
    code: string;
    message: string;
    details?: unknown;
    request_id: string;
  };
};

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});

if (!res.ok) {
  const body = (await res.json()) as ApiError;
  switch (body.error.code) {
    case "rate_limited": {
      const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      // retry
      break;
    }
    case "scope_required":
      // tell the operator to widen the key's scopes
      break;
    case "validation_failed":
      // surface body.error.details to the user
      break;
    case "unsupported_key_version":
      // you passed a v1 key - mint a v2 one
      break;
    default:
      console.error(
        "Productlane error",
        body.error.code,
        body.error.request_id,
      );
  }
}

Retry strategy

CodeRetry?
rate_limitedYes - honour Retry-After.
internal_errorYes - exponential backoff (1s, 2s, 4s, …), give up after a few attempts.
conflictSometimes - only if your retry resolves the conflict (e.g. re-fetch then re-submit).
Everything elseNo. A retry will fail the same way. Fix the request.