LettBestilt
Embed ordering on your restaurant's website. Money is integer øre (1 NOK = 100 øre). Timestamps are ISO-8601 UTC. Tenant key is the ?slug= query parameter. The OpenAPI spec is at /api/v1/openapi.json.
Write endpoints (POST /api/v1/orders, POST /api/v1/coupons/validate) require a public API key UNLESS the request originates from an allow-listed browser origin. Generate the key under Settings → API-tilgang. The plaintext is shown once; only a sha256 hash is stored.
curl -X POST 'https://lettbestilt.no/api/v1/orders?slug=demo' \
-H 'Authorization: Bearer lbs_…' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 6f5e0a3b' \
-d @order.jsonAn origin is allowed if any of:
NEXT_PUBLIC_APP_URL exactly.NEXT_PUBLIC_APP_DOMAIN or ends with .${NEXT_PUBLIC_APP_DOMAIN}.PUBLIC_API_ALLOWED_ORIGINS (comma-separated).Preflight is supported via OPTIONS on every route.Authorization and Idempotency-Key are in the allow-list.
Per (slug, ip). Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. 429 responses additionally include Retry-After.
| Endpoint | Limit |
|---|---|
POST /api/v1/orders | 10 / 60s |
POST /api/v1/coupons/validate | 30 / 60s |
POST /api/v1/coupons/preview-auto | 30 / 60s |
POST /api/v1/newsletter/signup | 5 / 60s |
GET /api/v1/orders/[token] | 60 / 60s (per IP) |
GET /api/v1/menu | No limit (cached, ETag) |
GET /api/v1/dine-in/availability | 60 / 60s |
All 4xx/5xx responses have the same shape:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"details": { "issues": [...] }
}
}POST /api/v1/orders honours an Idempotency-Keyheader. A replay of the same key (per restaurant) returns the existing order — useful when the embed's network drops mid-redirect.
Configure HTTPS endpoints to receive ORDER_CREATED, ORDER_PAID, ORDER_STATUS_CHANGED or ORDER_CANCELLED. Every delivery is signed with HMAC-SHA256:
X-LettBestilt-Webhook-Id: wh_…
X-LettBestilt-Webhook-Timestamp: 1746540000
X-LettBestilt-Webhook-Signature: sha256=<hex>
# signature = HMAC_SHA256(secret, `${timestamp}.${rawBody}`)3 retries with backoff (0s, 2s, 5s). 4xx responses are treated as permanent client errors and not retried; 5xx and timeouts retry.
GET /api/v1/restaurant?slug=Branding/contact only. Lighter alternative to /menu for header components.
GET /api/v1/menu?slug=Full payload: restaurant, menus, categories, products (variants, addons, allergens), coupons, allergens, upsell config, opening hours, delivery zones. Cache-Control + weak ETag — supports If-None-Match.
GET /api/v1/dine-in/availability?slug=&date=&partySize=Returnerer 15-minutters slots for valgt dato/gruppestørrelse. Hver slot har time (HH:mm Oslo), iso (UTC, send som dineInAt ved POST /api/v1/reservations) og available. Skjul slots med available: false i UI. Tom liste ⇒ restauranten er stengt eller ingen bord passer.
POST /api/v1/orders?slug=Create a takeaway order. Returns { orderId, publicToken } for cash, { orderId, publicToken, stripeUrl } for card, or { orderId, publicToken, vippsUrl } for Vipps. Redirect the customer to stripeUrl/vippsUrl to pay — read them as stripeUrl ?? vippsUrl and treat the names as historical, not as a promise about which provider processes the payment. Which methods are available is exposed as restaurant.payment ({ card, vipps, cash }) on GET /api/v1/restaurant.
Kun takeaway (15 % MVA på mat). orderType: "DINE_IN" returnerer 422 DINE_IN_NOT_SUPPORTED — bordbestilling går via POST /api/v1/reservations som forespørsel.
POST /api/v1/reservations?slug=Send en bordreservasjons-forespørsel. Returnerer status: "PENDING_APPROVAL" og tableId: null. Restauranten godkjenner eller avslår fra dashbordet — kunden får en e-post når status endres. Bord tildeles først ved godkjenning, så samme tidspunkt kan ha flere ventende forespørsler uten å blokkere hverandre. Bruk publicToken for å spore status via GET /api/v1/orders/{token}, eller bygg eget UI rundt webhook-eventene RESERVATION_REQUESTED, RESERVATION_APPROVED, RESERVATION_REJECTED.
Påkrevde felter: dineInAt (ISO-8601 UTC), partySize, guestName. Sterkt anbefalt: guestEmail (uten denne kan vi ikke varsle kunden om godkjenning/avslag).
Slipper du å integrere mot /api/v1/* selv: drop-in embed-en bygger bestillingsflyten som en iframe på lettbestilt.no, så du slipper å håndtere CORS, Bearer-tokens og rate-limits selv.
<script src="https://lettbestilt.no/embed.js" defer></script>
<div data-lettbestilt slug="demo" mode="checkout"></div>GET /api/v1/orders/[token]Public order tracking by publicToken (CUID, unguessable).
POST /api/v1/coupons/validateAlways returns 200. Body is either { valid: true, discount, coupon } or { valid: false, discount: 0, reason } with one of:
UNKNOWN_OR_INACTIVENOT_YET_VALIDEXPIREDEXHAUSTEDMINIMUM_NOT_METWRONG_DAY / WRONG_TIME / WRONG_FULFILLMENTNOT_APPLICABLE_TO_CARTSUBSCRIBER_ONLY — kupongen krever at email er en aktiv abonnent. Send email i body for å aktivere koden.ALREADY_REDEEMED — kupongen er engangs-per-kunde og denne email har allerede brukt den.POST /api/v1/coupons/preview-autoForhåndsviser hvilket automatisk tilbud (auto-promo) serveren vil anvende på kurven i dag. Bruk dette til å rendere en Tilbud: −X kr-linje i din egen kasse-oppsummering. Serveren re-evaluerer ved POST /api/v1/orders, så dette er kun for visning.
Returnerer { applies: true, discount, coupon } hvis et tilbud gjelder, ellers { applies: false, discount: 0 }. Husk å sende quantity og variantName per linje — FIXED_PRICE-tilbud med per-variant priser (Medium/Stor) trenger variantnavnet for å treffe.
curl -X POST 'https://lettbestilt.no/api/v1/coupons/preview-auto' \
-H 'Authorization: Bearer lbs_…' \
-H 'Content-Type: application/json' \
-d '{
"slug": "demo",
"subtotal": 58500,
"fulfillment": "PICKUP",
"cart": [
{ "productId": "prod_…", "quantity": 1, "variantName": "Stor", "lineTotal": 35000 },
{ "productId": "prod_…", "quantity": 1, "variantName": "Medium", "lineTotal": 23500 }
]
}'
# → { "applies": true, "discount": 8000,
# "coupon": { "code": "FASTPRIS", "displayName": "Mandag og tirsdagstilbud",
# "discountType": "FIXED_PRICE", "appliesTo": "CATEGORIES" } }POST /api/v1/newsletter/signup?slug=Idempotent påmelding til restaurantens nyhetsbrev. Returnerer en velkomstkode kunden kan bruke i kassen. Kupongen er typisk låst til abonnenter (requiresSubscriber: true) — bruk samme e-post når du senere kaller /coupons/validate eller /orders.
Konfigurasjonen ligger som restaurant.newsletterPopup i GET /api/v1/restaurant. Render popup-en kun når feltet ikke er null.
curl -X POST 'https://lettbestilt.no/api/v1/newsletter/signup?slug=demo' \
-H 'Authorization: Bearer lbs_…' \
-H 'Content-Type: application/json' \
-d '{"email":"kari@eksempel.no","name":"Kari Nordmann"}'
# → { "couponCode": "VELKOMMEN10", "discountType": "PERCENT", "discountValue": 10,
# "successMessage": "Bruk koden {{code}} i kassen for {{discount}}% rabatt." }