Skip to navigation

Errors, rate limits and retries

What can go wrong, and how to retry safely

Error format

Errors use one envelope:

{
"error": {
"code": "VALIDATION_FAILED",
"message": "Velg minst 1 fra Saus",
"details": {}
}
}

Branch on code, never on message. message is written for people and is often in Norwegian; for business rules (sold out, closed, outside the delivery zone) it is usually fine to show the customer as it is. details is only present for some codes.

CodeStatusMeaning
MISSING_SLUG400The slug is missing
MISSING_TOKEN400The order token is not in a valid format
INVALID_JSON400The body is not valid JSON
INVALID_SUCCESS_URL / INVALID_CANCEL_URL400A return URL is not https on a verified domain or lettbestilt.no
BAD_REQUEST400Malformed input, such as an invalid Idempotency-Key, or the restaurant is closed or does not offer the chosen fulfillment or payment method
UNAUTHORIZED401Missing or wrong API key, and the origin is not trusted
RESTAURANT_NOT_FOUND404No live restaurant has this slug
ORDER_NOT_FOUND404No order has this token
POPUP_NOT_AVAILABLE404Newsletter signup is turned off for the restaurant
PRICE_CHANGED409You sent expectedTotal and the server’s total is higher. details has expectedTotal, actualTotal and currency
PAYMENT_PENDING409The order exists but its payment link is still being created. Retry shortly with the same key
PAYMENT_INCOMPLETE409The order exists but never got a payment link. Start a new order
IDEMPOTENCY_CONFLICT409The Idempotency-Key was already used by the other endpoint, or (reservations only) with a different body
VALIDATION_FAILED422The body failed validation (details.issues lists the fields), or the order breaks a business rule
FULFILLMENT_NOT_ALLOWED422A product is pickup-only or delivery-only, and the order asked for the other
DINE_IN_DISABLED, PARTY_TOO_LARGE, TOO_EARLY422Reservation rejections. See Table reservations
RATE_LIMITED429Too many requests. See Retry-After
PAYMENT_PROVIDER_INVALID502The payment provider returned an unusable payment link
INTERNAL_ERROR5xxSomething failed on our side or at a provider. 503 on order creation means the payment provider did not answer

One exception to the envelope: 409 ONLINE_BOOKING_CLOSED from POST /reservations uses a flat shape, { "error": "…", "code": "ONLINE_BOOKING_CLOSED" }.

Rate limits

Limits are counted per client IP, and for most endpoints per restaurant as well:

EndpointLimit
POST /orders10 per minute per slug and IP
POST /reservations20 per minute per slug and IP
POST /coupons/validate, POST /coupons/preview-auto30 per minute per slug and IP
POST /newsletter/signup5 per minute per IP, and 200 per hour per restaurant
GET /dine-in/availability60 per minute per slug and IP
GET /address-search30 per minute per IP
GET /orders/{token}60 per minute per IP, and 30 per minute per token
GET /prep-time, GET /site-gate600 per minute per slug and IP
GET /menu, GET /restaurantNo limit; responses are cached at the edge

Rate-limited responses include these headers:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the window
X-RateLimit-RemainingRequests left in the window
X-RateLimit-ResetUnix time in seconds when the window resets
Retry-AfterSeconds to wait (only on 429)

When you call the API from your server, every customer shares your server’s IP. POST /orders then allows 10 orders per minute per restaurant from that server. Calls from the browser on a verified domain are counted per customer instead. Talk to LettBestilt if you expect more traffic than the limits allow.

Read the headers rather than hard-coding the numbers; they may change.

Idempotency

POST /orders and POST /reservations accept an Idempotency-Key header: 16 to 128 URL-safe characters (a-z, A-Z, 0-9, _, -). A UUID works well. Keys are scoped per restaurant, and orders and reservations share the same key space.

POST /ordersPOST /reservations
Same key, same bodyReturns the original order and payment linkReturns the original request (200, idempotent: true)
Same key, different bodyReturns the original order. The body is not compared409 IDEMPOTENCY_CONFLICT
Key already used by the other endpoint409 IDEMPOTENCY_CONFLICT409 IDEMPOTENCY_CONFLICT

Create one key per checkout attempt, store it with the attempt, and reuse it only to retry that attempt.

When to retry

SituationRetry?
Network error or timeout on POST /orders or POST /reservationsYes, with the same Idempotency-Key
409 PAYMENT_PENDINGYes, after a couple of seconds, with the same key
429 RATE_LIMITEDYes, after Retry-After seconds
503 on POST /ordersOnly as a new attempt with a new key; the first order has no payment link
500Yes, with backoff. On POST /orders and POST /reservations, reuse the same key. Contact LettBestilt if it persists
400, 401, 404, 422, other 409No. Change the request first