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 OKfor reads and updates,201 Createdwhen 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.
| Status | Meaning | When you'll see it |
|---|---|---|
200 | OK | Successful GET, PATCH, DELETE. |
201 | Created | A new member, order, inventory level, etc. was created. |
400 | Bad Request | Validation failed or the body/query is malformed. |
401 | Unauthorized | Missing, invalid or expired Bearer token. |
403 | Forbidden | Authenticated, but the role/tenant is not allowed — also used for plan/limit exceeded. |
404 | Not Found | The resource (or the tenant subdomain) does not exist. |
409 | Conflict | The request conflicts with the current state (e.g. duplicate resource). |
422 | Unprocessable Entity | The backend rejected semantically-invalid data. |
429 | Too Many Requests | Rate limit hit, on the API or on an upstream backend. |
500 | Internal Server Error | Unexpected failure inside the BFF. |
502 | Bad Gateway | An upstream backend (Medusa/Strapi) returned a 5xx. |
504 | Gateway Timeout | An 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
errorsmap keyed by field; for limit errors it containscurrentandlimit.
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, withcurrentandlimitindetails.
- 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"
}
}
Error messages are intended for developers and logs. Do not display error.message directly to end users — map error.code to a localized message in your UI instead.
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()
}