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

_tagStatusWhat it meansWhat to do
Unauthorized401The API key is missing, wrong or revoked. On GET /v1/me, the visitor's token is missing or expiredCheck the key. On /me, treat the visitor as signed out
NotFound404The thing you asked for doesn't exist in your organization. resource says what kind, id says whichCheck the slug, short code or id. Show your own "not found" page
Conflict409The request clashes with what's already there. See reasonOn 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
PaymentsNotReady409Your Stripe account can't take payments yetFinish connecting Stripe. Hide pay buttons until GET /v1/org?include=paymentsReady says true
SlotUnavailable409That booking time is taken or no longer open. startsAt echoes the timeFetch the slots again and let the person pick another
BookingClosed409The booking type no longer takes bookings. reason is archived, cancelled or pastStop offering it on your site
GroupTypeMismatch409The group id you sent is a different type than the type you sentSend the group's real type, or leave out id
LineUnavailable409A basket line can't be sold. variantId says which, reason says whyRemove 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
NoAddress422The email's contact has no email addressInclude at least one address in contact.emails
UploadRejected422The file type isn't accepted or the file is too big. reason is unsupported_type or too_large, message explainsShow message to the person
AnswersInvalid422Some booking questions are unanswered or answered in the wrong shape. questionIds lists themMark those questions on your form
ContactInfoRequired422The booking type needs an email or phone you didn't send. fields lists themAsk for the listed fields
ColumnTypeMismatch422A group column already exists with a different type. column, existing and given explainSend the column's existing type, or use a new column name
ForeignFile422A file cell points at a URL that isn't one of your organization's uploadsUpload the file with POST /v1/uploads first
TooManyRequests429You've hit the hourly limit for this kind of call. retryAfterSeconds says when it resetsWait that long, then retry. See Rate limits
CheckoutUnavailable502Stripe didn't open the payment page. Nothing was chargedRetry 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 limit above 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/event instead of /v1/events, returns an empty 404. A 404 with a NotFound body 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 key so 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:

lib/lahuta.js
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
}
app/book/actions.js
"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.

On this page