Errors

When a request fails, the API returns a non-2xx status code and a JSON body with an error message. Every endpoint follows the same rules, so one error handler covers the whole API.

Error format

The body of an error response is a JSON object with a human-readable string:

json
{ "error": "The 'query' field must be a non-empty string." }

Messages are written for developers. Log them and show them in internal tools, but base your control flow on the HTTP status rather than the exact wording.

When a validation failure has more than one cause, the body also carries details, with one entry per problem:

Field Shape
details An array of { path, message } objects. path names the field, such as config.storeUrl or variants[0].available. Item upload adds index, the product's position in your request, or -1 for request-wide problems.
json
{
  "error": "Validation failed.",
  "details": [
    { "index": 0, "path": "price", "message": "Price must be a non-negative number." },
    { "index": 2, "path": "variants[0].available", "message": "Expected boolean." }
  ]
}

A successful status never carries an error. If you receive a 2xx, the request did what it says.

Metered endpoints include the X-RateLimit-* headers on error responses as well. See Rate limits and quotas.

Status codes

Status Meaning
400 The request body is not valid JSON, or is not a JSON object.
401 The API key is missing, malformed, the wrong type, not recognized, or belongs to an organization that no longer exists.
403 The key is valid, but the organization has no active subscription, no quota record, or no access to this endpoint on its plan; or the key is limited to websites or IP addresses and the request came from somewhere else.
404 The resource in the path doesn't exist, or belongs to another organization.
409 The request conflicts with existing state, such as starting a sync on a data source that is not active.
410 The endpoint has been retired.
413 The request is too large, such as more than 5,000 products in one upload.
415 An uploaded file's type is not accepted.
422 The request is well-formed, but a body field, query parameter or path parameter failed validation.
429 A quota or requests-per-minute limit was hit. Check Retry-After.
500 Something failed on the server. The message is always Internal server error.
502 The stylist's AI model failed or returned output that could not be read.
503 A dependency isn't ready yet, such as a catalogue index being rebuilt. Retry shortly.

Any endpoint can also return the shared authentication, entitlement, rate-limit and server errors. Those are listed with each endpoint and explained in Authentication and Rate limits and quotas.

Responses that report per-part results

Two kinds of response describe outcomes more finely than a single status can.

Multi-file uploads

Image upload returns 200 when every file was stored, 207 Multi-Status when some were, and an error status when none were. The body always contains files, failures and summary, so read failures whenever the status is 207.

Streaming responses

Once a streaming response has started, its HTTP status has already been sent as 200. A failure after that point arrives as an error event inside the stream, with the same { "error": "..." } body. Failures before the stream starts, such as validation or rate limits, are ordinary error responses. See Streaming.

Handling errors in JavaScript

This helper reads the body once and exposes the status, message, validation details and retry hint:

javascript
class StylorError extends Error {
  constructor(status, body, headers) {
    super(body?.error ?? `HTTP ${status}`);
    this.name = "StylorError";
    this.status = status;
    this.body = body;
    this.details = body?.details ?? null;
    const retryAfter = headers.get("Retry-After");
    this.retryAfterSeconds = retryAfter ? Number(retryAfter) : null;
  }

  get isRetryable() {
    return this.status === 429 || this.status >= 500;
  }
}

async function stylor(path, { method = "GET", body, key = process.env.STYLOR_API_KEY } = {}) {
  const res = await fetch(`https://stylor.ai/api${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const text = await res.text();
  let data = null;
  try {
    data = text ? JSON.parse(text) : null;
  } catch {
    data = { error: text || res.statusText };
  }

  if (!res.ok) throw new StylorError(res.status, data, res.headers);
  return data;
}

try {
  const { dataset } = await stylor(`/v1/datasets/${datasetId}`);
  console.log(dataset.name);
} catch (err) {
  if (err instanceof StylorError) {
    console.error(err.status, err.message, err.details ?? "");
    if (err.isRetryable && err.retryAfterSeconds) {
      // Wait before retrying. See the rate limits guide for a backoff helper.
    }
  } else {
    throw err;
  }
}

Handling errors in Python

The same approach with requests:

python
import os
import requests

BASE_URL = "https://stylor.ai/api"


class StylorError(Exception):
    def __init__(self, status, body, headers):
        body = body or {}
        super().__init__(body.get("error") or f"HTTP {status}")
        self.status = status
        self.body = body
        self.details = body.get("details")
        retry_after = headers.get("Retry-After")
        self.retry_after_seconds = int(retry_after) if retry_after else None

    @property
    def is_retryable(self):
        return self.status == 429 or self.status >= 500


def stylor(path, method="GET", json=None, key=None):
    key = key or os.environ["STYLOR_API_KEY"]
    res = requests.request(
        method,
        f"{BASE_URL}{path}",
        headers={"Authorization": f"Bearer {key}"},
        json=json,
        timeout=60,
    )

    try:
        data = res.json() if res.content else None
    except ValueError:
        data = {"error": res.text or res.reason}

    if not res.ok:
        raise StylorError(res.status_code, data, res.headers)
    return data


try:
    dataset = stylor(f"/v1/datasets/{dataset_id}")["dataset"]
    print(dataset["name"])
except StylorError as err:
    print(err.status, err, err.details or "")

Retrying safely

  • Retry 429, 500, 502 and 503 responses after a delay, honoring Retry-After when it is present.
  • Don't retry 400, 401, 403, 404, 409, 410, 413, 415 or 422 without changing the request.
  • Item uploads and sync job creation are safe to repeat. A retried upload whose first attempt succeeded reports its products as unchanged, and creating a sync job returns the job already in flight.