Skip to navigation

Coupons and promotions

Show discounts in the cart before checkout

Restaurants have two kinds of offers:

  • Coupon codes that the customer types, listed in coupons in GET /menu.
  • Automatic promotions that need no code, listed in promotions in GET /menu. Show them as banners.

Neither list includes the code itself. An order gets either the code the customer typed or the best automatic promotion, never both. The server always calculates the final discount when the order is created; the endpoints below are for showing it in the cart.

The TypeScript examples use API, SLUG and headers from the shared setup.

Send the cart

Both endpoints take the restaurant’s slug in the JSON body, the subtotal in øre and the cart lines. Send the same cart you will order:

FieldWhy
cart[].productId, categoryId, lineTotal, quantityOffers can be limited to products or categories, and “X for Y kr” offers count units. categoryId is required for validate and optional for preview-auto
cart[].variantNameNeeded for offers with a price per size, such as Medium or Stor
fulfillment, deliveryFeeOffers can be limited to pickup or delivery, or give free delivery
requestedPickupAtDay and time rules are checked against the pickup time for a pre-order. Send the same value as on the order, or the cart can show a discount the order rejects

Preview the automatic promotion

curl -X POST "https://www.lettbestilt.no/api/v1/coupons/preview-auto" \
-H "Authorization: Bearer $LETTBESTILT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "pizzaroma",
"subtotal": 36800,
"fulfillment": "PICKUP",
"cart": [{ "productId": "PRODUCT_ID", "quantity": 2, "variantName": "Stor", "lineTotal": 36800 }]
}'

The response is { "applies": true, "discount": 6900, "coupon": { ... } }, or { "applies": false, "discount": 0 } when no promotion matches.

Validate a code

curl -X POST "https://www.lettbestilt.no/api/v1/coupons/validate" \
-H "Authorization: Bearer $LETTBESTILT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "pizzaroma",
"code": "VELKOMMEN",
"subtotal": 33800,
"cart": [{ "productId": "PRODUCT_ID", "categoryId": "CATEGORY_ID", "quantity": 2, "lineTotal": 33800 }],
"email": "kari@example.com"
}'

A well-formed request always gets 200; read valid. When it is false, reason says why and message is a Norwegian sentence you can show the customer as it is. When the code is valid, send it as couponCode on POST /orders.

reasonMeaning
UNKNOWN_OR_INACTIVEThe code does not exist or is turned off
NOT_YET_VALIDThe code’s start date is in the future
EXPIREDThe code has expired
EXHAUSTEDThe code has reached its usage limit
MINIMUM_NOT_METThe cart is below the minimum order amount
WRONG_FULFILLMENTThe code is only valid for pickup or only for delivery
NOT_APPLICABLE_TO_CARTThe code applies to products or categories that are not in the cart, or an “X for Y kr” offer has too few items
WRONG_DAY / WRONG_TIMEThe code is limited to certain days or times
SUBSCRIBER_ONLYThe code is for newsletter subscribers, matched on email
ALREADY_REDEEMEDThe code is once per customer, and this email has used it

Send email when you have it. Without it, subscriber-only codes always fail, and once-per-customer codes are only checked when the order is created.

Discount types

discountTypediscountValue
PERCENTPercent off
FIXEDØre off
FIXED_PRICEA fixed price in øre for the products it applies to
FREE_DELIVERYFree delivery
BUNDLE”X for Y kr”: the price in øre for one bundle of bundleQuantity units, not the discount

Always show discount from the response as the amount saved. Treat unknown types as a general offer, since more may be added.

Newsletter signup

POST /newsletter/signup?slug= adds the customer to the restaurant’s newsletter and returns the welcome coupon (couponCode, discountType, discountValue and the restaurant’s successMessage, where you replace {{code}} and {{discount}}).

It only works when the restaurant has turned on its newsletter popup with an active coupon; otherwise you get 404 POPUP_NOT_AVAILABLE. Check restaurant.newsletterPopup from GET /restaurant first. It is null when signup is off, and otherwise holds the popup’s texts and when to show it.