Errors
What each error response means, which status it comes with, and what your code should do about it.
When a request fails, the Org API answers with an HTTP status and, for most errors, a JSON body. The body's _tag names the error, and some errors carry extra fields that tell you exactly what was wrong.
{
"_tag": "NotFound",
"resource": "event",
"id": "k7Qm2x"
}Check the status first, then switch on _tag for the details.
Error reference
_tag | Status | What it means | What to do |
|---|---|---|---|
Unauthorized | 401 | The API key is missing, wrong or revoked. On GET /v1/me, the visitor's token is missing or expired | Check the key. On /me, treat the visitor as signed out |
NotFound | 404 | The thing you asked for doesn't exist in your organization. resource says what kind, id says which | Check the slug, short code or id. Show your own "not found" page |
Conflict | 409 | The request clashes with what's already there. See reason | On POST /v1/checkouts, the key's checkout was already paid or has expired: use a new key. On POST /v1/products/checkouts, the orgSlug organization isn't listed on Discover |
PaymentsNotReady | 409 | Your Stripe account can't take payments yet | Finish connecting Stripe. Hide pay buttons until GET /v1/org?include=paymentsReady says true |
SlotUnavailable | 409 | That booking time is taken or no longer open. startsAt echoes the time | Fetch the slots again and let the person pick another |
BookingClosed | 409 | The booking type no longer takes bookings. reason is archived, cancelled or past | Stop offering it on your site |
GroupTypeMismatch | 409 | The group id you sent is a different type than the type you sent | Send the group's real type, or leave out id |
LineUnavailable | 409 | A basket line can't be sold. variantId says which, reason says why | Remove the line. reason is not_found, not_for_sale, wrong_org or missing_weight. The last one means the product needs a weight in the dashboard before it can ship |
NoAddress | 422 | The email's contact has no email address | Include at least one address in contact.emails |
UploadRejected | 422 | The file type isn't accepted or the file is too big. reason is unsupported_type or too_large, message explains | Show message to the person |
AnswersInvalid | 422 | Some booking questions are unanswered or answered in the wrong shape. questionIds lists them | Mark those questions on your form |
ContactInfoRequired | 422 | The booking type needs an email or phone you didn't send. fields lists them | Ask for the listed fields |
ColumnTypeMismatch | 422 | A group column already exists with a different type. column, existing and given explain | Send the column's existing type, or use a new column name |
ForeignFile | 422 | A file cell points at a URL that isn't one of your organization's uploads | Upload the file with POST /v1/uploads first |
TooManyRequests | 429 | You've hit the hourly limit for this kind of call. retryAfterSeconds says when it resets | Wait that long, then retry. See Rate limits |
CheckoutUnavailable | 502 | Stripe didn't open the payment page. Nothing was charged | Retry with the same key |
Errors without a body
A few responses have no JSON body at all:
- 400 Bad Request. The request didn't match the expected shape: a missing required field, a phone number that isn't in E.164 format, a
limitabove the maximum, a malformed id. Compare your request with the endpoint in the API reference. - 404 on an unknown path. A typo in the URL, like
/v1/eventinstead of/v1/events, returns an empty404. A404with aNotFoundbody means the path was right but the item wasn't found. - 5xx. Something went wrong on Lahuta's side. Retry after a short wait. For calls that send or charge, retry with the same idempotency
keyso nothing happens twice.
Handle errors in code
This small helper throws on any error and keeps the status and body, so the calling code can decide what to show:
export async function lahuta(path, { method = "GET", body } = {}) {
const res = await fetch(`https://api.lahuta.org/v1${path}`, {
method,
headers: {
"X-Api-Key": process.env.LAHUTA_API_KEY,
...(body && { "Content-Type": "application/json" }),
},
body: body && JSON.stringify(body),
})
const data = await res.json().catch(() => null)
if (!res.ok) {
const error = new Error(data?._tag ?? `Lahuta API error ${res.status}`)
error.status = res.status
error.body = data
throw error
}
return data
}"use server"
import { lahuta } from "@/lib/lahuta"
export async function bookSlot(input) {
try {
return {
booking: await lahuta("/bookings", { method: "POST", body: input }),
}
} catch (error) {
switch (error.body?._tag) {
case "SlotUnavailable":
return {
error: "Someone took that time a moment ago. Please pick another.",
}
case "AnswersInvalid":
return {
error: "Please check the highlighted questions.",
questionIds: error.body.questionIds,
}
default:
throw error
}
}
}Log the full body of unexpected errors on your server. Don't pass raw error bodies to visitors: they're written for developers.