Errors

This guide describes what happens when a request to the Cannahub API fails. Every error is returned with an appropriate HTTP status code and a consistent JSON body, so you can branch on the status code first and use the machine-readable code for finer-grained handling.

The BFF (/api/*) normalizes both its own validation errors and errors bubbling up from the Medusa/Strapi backends into a single shape. Upstream statuses are preserved where it matters (for example a backend 429 stays a 429, and an upstream 5xx is surfaced as 502) so the client can react correctly instead of seeing everything as a generic 500.


Status codes

Check the HTTP status code first to know whether your request succeeded.

  • Name
    2xx
    Description

    Success. 200 OK for reads and updates, 201 Created when a resource was created.

  • Name
    4xx
    Description

    Client error — the request was rejected because of something in the request (bad input, missing/expired token, insufficient permissions, a conflict, or too many requests).

  • Name
    5xx
    Description

    Server error — the request was well-formed but the API or an upstream backend failed to fulfil it.

StatusMeaningWhen you'll see it
200OKSuccessful GET, PATCH, DELETE.
201CreatedA new member, order, inventory level, etc. was created.
400Bad RequestValidation failed or the body/query is malformed.
401UnauthorizedMissing, invalid or expired Bearer token.
403ForbiddenAuthenticated, but the role/tenant is not allowed — also used for plan/limit exceeded.
404Not FoundThe resource (or the tenant subdomain) does not exist.
409ConflictThe request conflicts with the current state (e.g. duplicate resource).
422Unprocessable EntityThe backend rejected semantically-invalid data.
429Too Many RequestsRate limit hit, on the API or on an upstream backend.
500Internal Server ErrorUnexpected failure inside the BFF.
502Bad GatewayAn upstream backend (Medusa/Strapi) returned a 5xx.
504Gateway TimeoutAn upstream request timed out.

Error response shape

Every error response has a top-level error object with a code, a human-readable message, and an optional details object carrying extra context (for example per-field validation errors).

  • Name
    error.code
    Type
    string
    Description

    Machine-readable error code. One of the values in the table below — branch on this for programmatic handling.

  • Name
    error.message
    Type
    string
    Description

    Human-readable description of what went wrong. Safe to log; not meant to be shown verbatim to end users.

  • Name
    error.details
    Type
    object
    Description

    Extra context. For validation errors this contains an errors map keyed by field; for limit errors it contains current and limit.

Error response shape

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": {
      "errors": {
        "email": ["Invalid email"]
      }
    }
  }
}

Error codes

  • Name
    UNAUTHORIZED / TOKEN_EXPIRED / INVALID_TOKEN
    Description

    Authentication problem — 401. The Bearer token is missing, malformed or expired.

  • Name
    FORBIDDEN / INSUFFICIENT_PERMISSIONS
    Description

    The caller is authenticated but not allowed to perform this action — 403.

  • Name
    LIMIT_EXCEEDED
    Description

    A plan/subscription limit was reached (e.g. maximum number of members) — 403, with current and limit in details.

  • Name
    NOT_FOUND / RESOURCE_NOT_FOUND
    Description

    The requested resource does not exist — 404.

  • Name
    VALIDATION_ERROR / INVALID_REQUEST / MISSING_REQUIRED_FIELD
    Description

    The request body or query parameters failed validation — 400.

  • Name
    CONFLICT / DUPLICATE_RESOURCE
    Description

    The request conflicts with the current state of the resource — 409.

  • Name
    INTERNAL_ERROR
    Description

    Unexpected server error — 500.

  • Name
    BACKEND_ERROR
    Description

    An upstream backend (Medusa/Strapi) failed — surfaced as 502.

  • Name
    SERVICE_UNAVAILABLE
    Description

    Upstream unavailable — used for backend rate limiting (429) and upstream timeouts (504).

  • Name
    NOT_IMPLEMENTED
    Description

    The requested feature is not implemented — 501.


Examples

400 — Validation error

Returned when the request body or query fails validation. When Zod validation is used, details.errors maps each invalid field to its messages.

Request

curl -X POST https://api.cannahub.tech/api/whatsapp/link \
  -H "Authorization: Bearer {token}" \
  -H "Tenant-id: high-up" \
  -H "Content-Type: application/json" \
  -d '{ "email": "not-an-email" }'

Response · 400

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": {
      "errors": {
        "customerId": ["Required"],
        "email": ["Invalid email"],
        "token": ["Required"]
      }
    }
  }
}

401 — Unauthorized

The Bearer token is missing, invalid or expired.

Response · 401

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Unauthorized"
  }
}

403 — Forbidden

The token is valid but the caller lacks permission — for example a member hitting an admin-only endpoint, or a request scoped to the wrong tenant.

Response · 403

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Forbidden"
  }
}

A plan limit being reached is also a 403, but uses the LIMIT_EXCEEDED code and includes usage in details:

Response · 403 (limit exceeded)

{
  "error": {
    "code": "LIMIT_EXCEEDED",
    "message": "members limit exceeded",
    "details": {
      "current": 500,
      "limit": 500
    }
  }
}

404 — Not found

The resource does not exist (or was already consumed/deleted).

Response · 404

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Link code not found"
  }
}

409 — Conflict

The request conflicts with the current state of the resource, e.g. creating a resource that already exists. Conflicts forwarded from the backend keep the upstream message when available.

Response · 409

{
  "error": {
    "code": "CONFLICT",
    "message": "Member conflict"
  }
}

422 — Unprocessable entity

The payload is syntactically valid but semantically rejected by the backend.

Response · 422

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "stocked_quantity must be greater than or equal to 0"
  }
}

429 — Too many requests

Rate limit reached. Back off and retry after a short delay. When the limit comes from an upstream backend it is forwarded with the SERVICE_UNAVAILABLE code and a 429 status.

Response · 429

{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Too many requests to the backend, please retry shortly"
  }
}

500 / 502 / 504 — Server and upstream errors

500 is an unexpected failure inside the BFF. Failures originating in Medusa/Strapi are surfaced as 502 (BACKEND_ERROR), and upstream timeouts as 504 (SERVICE_UNAVAILABLE), so you can distinguish "our bug" from "the backend is down".

Server errors

{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Failed to fetch inventory items"
  }
}

Handling errors on the client

Reading the error shape

async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`https://api.cannahub.tech${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${token}`,
      'Tenant-id': 'high-up',
      ...init?.headers,
    },
  })

  if (!res.ok) {
    const body = await res.json().catch(() => null)
    const code = body?.error?.code ?? 'UNKNOWN'
    const message = body?.error?.message ?? res.statusText

    if (res.status === 401) redirectToLogin()
    if (code === 'LIMIT_EXCEEDED') showUpgradePrompt(body.error.details)

    throw new ApiError(res.status, code, message, body?.error?.details)
  }

  return res.json()
}

Was this page helpful?