Sellvik / developers
Concepts

Errors

The shape of every error response, and what each code means.

Every error from the Sellvik API has the same JSON shape. Codes are stable across versions — branch on error.code, never on error.message.

Shape

{
  "error": {
    "code": "invalid_body",
    "message": "Optional human-readable explanation. Will not be present on every error.",
    "issues": [
      { "path": ["price"], "message": "Expected string, received number" }
    ]
  }
}
  • error.code — machine-readable string. Stable.
  • error.message — human-readable text. May change between versions.
  • error.issues — present on 400 invalid_body from Zod validation. Per-field problems.
  • Additional fields (e.g. retryAfter on 429, required on 403 insufficient_scope) may be present per code.

HTTP status codes

We use a small, conventional subset.

StatusMeaning
200OK.
201Created — for POST that yields a new resource.
204No content — for DELETE and a few POST revocation endpoints.
400Validation failed (bad body, bad query). Look at error.issues.
401Missing, malformed, or revoked credential.
403Credential is valid but lacks the required capability or scope.
404Resource doesn't exist in this shop. (Wrong shop ⇒ also 404.)
409Conflict — duplicate slug, in-use category, etc.
429Rate limited. Honour the Retry-After header.
500We broke something. Filed automatically in our error tracker.

We never return 418.

Catalogue of error codes

The codes below are documented; new ones may appear over time (additive change). Treat unknown codes as a generic error of their HTTP class.

Authentication (401)

CodeWhen
missing_or_malformed_authorizationNo Authorization header on an admin route.
missing_publishable_keyNo X-Sellvik-Key on a store route.
invalid_keyKey not found, revoked, or wrong type for route.
invalid_customer_tokenCustomer JWT failed verification. See reason.
missing_customer_tokenRoute requires customer JWT; none was sent.

invalid_customer_token includes a reason:

ReasonCause
expiredAccess token TTL elapsed. Refresh it.
bad_signatureToken tampered with or signed with wrong secret.
wrong_audienceToken issued for a different shop.
wrong_kindRefresh token sent as access token, or vice versa.
malformedNot a valid JWT structure.
invalidGeneric catch-all on /auth/refresh.
revokedRefresh token chain was revoked (e.g. after replay).
replayedRefresh token already used. Revokes the family.

Authorization (403)

CodeDetail fieldWhen
insufficient_scoperequired: "<scope>"Admin key missing the scope this route needs.
headless_mode_disabledPublishable key, but shop hasn't enabled headless mode.
origin_not_allowedCORS preflight from an origin not in allowlist.

Validation (400)

CodeWhen
invalid_bodyRequest body failed Zod validation. issues has the why.
invalid_queryQuery string failed validation.
invalid_paramsPath parameter failed validation (e.g. malformed UUID).

Resource (404, 409)

CodeStatusWhen
not_found404Resource doesn't exist in this shop.
duplicate_slug409A product/category with that slug already exists.
has_subcategories409Can't delete a category that still has children.
email_exists409Customer signup with an email already on file (per shop).

Rate limit (429)

CodeDetail
rate_limitedretryAfter: <seconds>, limit: <per-minute>

The Retry-After response header carries the same number — prefer it over parsing the body.

Server (500)

CodeWhen
internal_errorUnhandled exception in the handler. Logged on our side.
const res = await fetch(url, init)
if (!res.ok) {
  const { error } = await res.json()
  switch (error.code) {
    case "rate_limited":
      await sleep(Number(res.headers.get("Retry-After") ?? 1) * 1000)
      // retry
      break
    case "invalid_customer_token":
      if (error.reason === "expired") await refreshAccessToken()
      break
    case "invalid_body":
      console.error("validation failed", error.issues)
      throw new Error("client bug")
    default:
      throw new Error(`${res.status} ${error.code}`)
  }
}

On this page