> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.lettbestilt.no/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.lettbestilt.no/_mcp/server.

# Change or publish a menu

PATCH https://www.lettbestilt.no/api/v1/partner/restaurants/{slug}/menus/{menuId}
Content-Type: application/json

**Partner key + restaurant key.** The only call that puts a menu in service. `{ "isActive": true, "isDefault": true }` makes it the main menu, and the previous default menu stops being the default in the same transaction, but only when the menu is or becomes active and has no date period. `{ "isActive": false }` takes it out of service. Omitted fields are unchanged. `priority` (0–100) decides which menu is shown when several have a schedule window at the same time: the highest wins, and with equal priority all are shown. Reordering the menus in the restaurant dashboard rewrites it (top is highest).

A menu with a date period (`startsOn`/`endsOn`) is published in one of three ways:

1. **Next to the main menu** in the period: a date period and `isDefault: true`. The main menu stays the default (`defaultKept: true`) and both are shown.
2. **Instead of the main menu** in the period: a date period without `isDefault`, a `priority` higher than the main menu's, and a schedule with all seven days 00:00–23:59 (`PUT …/schedules`). Both are needed: without the schedule the menu is never shown, and when the main menu has a schedule of its own, equal priority shows both and a lower one shows only the main menu.
3. **A new main menu**: `isDefault: true` without a date period. The previous main menu stops being the default.

A menu with a date period but without `isDefault` and without a schedule is never shown while another menu is the default.

`warnings` says when no menu is visible afterwards, or when another menu becomes visible. `name` is cleaned like category names (`textCleaned` in the response). Menus have no `updatedAt`: a change that collides with a concurrent change gives 409 `CHANGED`. `GET /api/v1/menu` is CDN-cached (`s-maxage=30, stale-while-revalidate=300`, at most 330 seconds), so the website can show the old menu for up to six minutes. Among the active menus inside their date period: when one or more have a schedule window right now, only those are shown (the highest `priority`; equal priority shows all), also instead of the default menu. Otherwise the default menu is shown, also when it has a schedule of its own and is outside it. A menu with a schedule that is not the default is therefore shown only inside its windows, and the main menu must be the default to be shown the rest of the time. With no active default menu, all active menus are shown outside the windows. `isLiveNow` says whether the menu is shown right now. The response has `notice` with this when the change puts the menu in or out of service, and the CDN text for any other change to an active menu.

Reference: https://docs.lettbestilt.no/api-reference/partner/update-partner-menu

## Authentication

- `Authorization` header (bearer token, required) — The partner key (`lbp_test_…` or `lbp_live_…`), sent as `Authorization: Bearer lbp_…` and created in LettBestilt Partner under **Partner key**. It belongs to the whole partner, not to a restaurant, and only works against `/api/v1/partner`. Server to server only: a request with an `Origin` header is rejected with `403 FORBIDDEN`, and the key must never be in a browser. A TEST key sees and creates only sandboxes; LIVE keys work once LettBestilt Partner is open. A suspended or archived partner, a revoked key and a restaurant key (`lbs_…`) here give `401 UNAUTHORIZED`; an expired key gives `401 KEY_EXPIRED`.
- `X-LettBestilt-Restaurant-Key` header (required) — The restaurant's own key (`lbs_test_…` or `lbs_live_…`) for calls about one restaurant in the partner API, sent together with the partner key. The key must have full access (scope `*`, otherwise `403 INSUFFICIENT_SCOPE`), belong to a restaurant the partner owns and be in the same environment as the partner key. A restaurant that does not exist, belongs to another partner or does not match the key always gives the same `404 RESTAURANT_NOT_FOUND`. Older keys without an environment in the prefix (`lbs_<random>`) are not accepted here; rotate them in LettBestilt Partner.

## Request

### Path parameters

- `slug` (string, required) — The restaurant's slug. It must match the restaurant key in `X-LettBestilt-Restaurant-Key`.
- `menuId` (string, required) — The menu's `id`.

### Body (application/json)

This endpoint expects a PartnerMenuUpdate.

- `name` (string, optional) — Cleaned like category names: links, email addresses, HTML, `javascript:` and invisible direction marks are removed (`textCleaned` in the response). Empty after cleaning gives 422.
- `isActive` (boolean, optional)
- `isDefault` (boolean, optional) — true makes the other default menus non-default in the same transaction, but only when the menu is or becomes active and has no date period (`startsOn` and `endsOn` null). A menu with a date period never takes the default from the main menu (`defaultKept: true` in the response). A menu with a date period (`startsOn`/`endsOn`) is published in one of three ways: 1. **Next to the main menu** in the period: a date period and `isDefault: true`. The main menu stays the default (`defaultKept: true`) and both are shown. 2. **Instead of the main menu** in the period: a date period without `isDefault`, a `priority` higher than the main menu's, and a schedule with all seven days 00:00–23:59 (`PUT …/schedules`). Both are needed: without the schedule the menu is never shown, and when the main menu has a schedule of its own, equal priority shows both and a lower one shows only the main menu. 3. **A new main menu**: `isDefault: true` without a date period. The previous main menu stops being the default. A menu with a date period but without `isDefault` and without a schedule is never shown while another menu is the default.
- `startsOn` (date, optional, nullable) — YYYY-MM-DD (Europe/Oslo), inclusive. null = no limit.
- `endsOn` (date, optional, nullable) — Cannot be before `startsOn` (checked against the stored menu).
- `priority` (integer, optional) — The menu's priority (0–100). When several menus have a schedule window at the same time, the one with the highest priority is shown; with equal priority all are shown. Reordering the menus in the restaurant dashboard rewrites the priority (top = highest).

## Response

### 200

The menu after the change.

- `data` (PartnerMenuUpdated, required) — A menu in list form. Among the active menus inside their date period: when one or more have a schedule window right now, only those are shown (the highest `priority`; equal priority shows all), also instead of the default menu. Otherwise the default menu is shown, also when it has a schedule of its own and is outside it. A menu with a schedule that is not the default is therefore shown only inside its windows, and the main menu must be the default to be shown the rest of the time. With no active default menu, all active menus are shown outside the windows. `isLiveNow` says whether the menu is shown right now. `Menu` has no `updatedAt`: a change that collides with a concurrent change gives 409 `CHANGED`.

## Errors

### 400 Bad Request Error

`INVALID_JSON`: the body is not valid JSON.

- `error` (ErrorBody, required)

### 401 Unauthorized Error

`UNAUTHORIZED`: a key is missing, invalid or revoked, the two keys are from different environments (TEST and LIVE), or the partner is suspended or archived. `KEY_EXPIRED`: a key is past its expiry date.

- `error` (ErrorBody, required)

### 403 Forbidden Error

`FORBIDDEN`: the request has an `Origin` header (server to server only). `PARTNER_AGREEMENT_REQUIRED`: a LIVE key, but the partner agreements are not signed. `PARTNER_NOT_APPROVED`: LettBestilt has not approved the partner yet (all keys). `INSUFFICIENT_SCOPE`: the restaurant key does not have full access (`error.details.requiredScope` is `"*"`).

- `error` (ErrorBody, required)

### 404 Not Found Error

`RESTAURANT_NOT_FOUND` (the slug does not exist, belongs to another partner or does not match the restaurant key), or `MENU_NOT_FOUND` (the menu does not exist or belongs to another restaurant).

- `error` (ErrorBody, required)

### 409 Conflict Error

`CHANGED` (the menu changed at the same time; read it again and try again), `RESTAURANT_ARCHIVED` (the restaurant is archived) or `DEMO_RESTAURANT` (the demo restaurant cannot be changed through the API).

- `error` (ErrorBody, required)

### 422 Unprocessable Entity Error

`VALIDATION_FAILED` with `error.details.issues`: an empty body, an unknown field, an invalid date, `endsOn` before `startsOn`, or a `priority` that is not a whole number from 0 to 100.

- `error` (ErrorBody, required)

### 429 Too Many Requests Error

`RATE_LIMITED`: more than 120 calls per minute for this partner key, or more than 600 per minute for the partner across all its keys (reads and writes). Writes also share a bucket of 30 changes per minute per partner and environment. See `Retry-After`.

- `error` (ErrorBody, required)

## Types

### PartnerMenuUpdated

A menu in list form. Among the active menus inside their date period: when one or more have a schedule window right now, only those are shown (the highest `priority`; equal priority shows all), also instead of the default menu. Otherwise the default menu is shown, also when it has a schedule of its own and is outside it. A menu with a schedule that is not the default is therefore shown only inside its windows, and the main menu must be the default to be shown the rest of the time. With no active default menu, all active menus are shown outside the windows. `isLiveNow` says whether the menu is shown right now. `Menu` has no `updatedAt`: a change that collides with a concurrent change gives 409 `CHANGED`.

- `id` (string, required)
- `name` (string, required)
- `isActive` (boolean, required) — Off = never shown.
- `isDefault` (boolean, required) — The default menu is shown when no menu has a schedule window right now. The partner API keeps at most one without a date period.
- `isLiveNow` (boolean, required) — Among the menus `/api/v1/menu` shows right now (without the CDN delay).
- `priority` (integer, required) — The menu's priority (0–100). When several menus have a schedule window at the same time, the one with the highest priority is shown; with equal priority all are shown. Reordering the menus in the restaurant dashboard rewrites the priority (top = highest).
- `sortOrder` (integer, required)
- `startsOn` (date, required, nullable) — YYYY-MM-DD (Europe/Oslo), inclusive. null = no limit.
- `endsOn` (date, required, nullable) — YYYY-MM-DD (Europe/Oslo), inclusive. null = no limit.
- `schedules` (list of PartnerMenuSchedule, required)
- `categoryCount` (integer, required)
- `productCount` (integer, required) — Active products.
- `publication` (enum, required) — PUBLISHED when the change turned a menu on or made an active menu the default; UNPUBLISHED when an active menu was turned off.
  - Allowed values: `PUBLISHED`, `UNPUBLISHED`
- `previousDefaultMenuIds` (list of string, required) — The menus that were the default and no longer are.
- `defaultKept` (boolean, required) — true when the menu has a date period and the other default menus were therefore kept.
- `warnings` (list of PartnerMenuUpdatedWarningsItems, required) — `[{ path, message }]` with an empty path: when no menu is visible after the change, or when another menu becomes visible (for example the lunch menu all day because the only default menu was taken out). Computed for right now.
- `textCleaned` (integer, required) — 1 when the cleaning removed something from `name`, otherwise 0.
- `notice` (string, required, nullable) — On PUBLISHED/UNPUBLISHED: whether the menu is shown, the CDN cache and the default-menu rule. Any other change to an active menu (name, priority, dates) has the CDN text. null only for a menu that is and stays off.

### ErrorBody

- `code` (enum, required) — A machine-readable error code.
  - Allowed values: `MISSING_SLUG`, `MISSING_TOKEN`, `INVALID_JSON`, `INVALID_SUCCESS_URL`, `INVALID_CANCEL_URL`, `VALIDATION_FAILED`, `RESTAURANT_NOT_FOUND`, `ORDER_NOT_FOUND`, `RATE_LIMITED`, `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `INSUFFICIENT_SCOPE`, `KEY_EXPIRED`, `POPUP_NOT_AVAILABLE`, `PRICE_CHANGED`, `PAYMENT_INCOMPLETE`, `PAYMENT_PENDING`, `PAYMENT_PROVIDER_INVALID`, `FULFILLMENT_NOT_ALLOWED`, `IDEMPOTENCY_CONFLICT`, `DINE_IN_DISABLED`, `PARTY_TOO_LARGE`, `TOO_EARLY`, `NO_TABLE`, `INVALID_PARTY_SIZE`, `INTERNAL_ERROR`, `PARTNER_AGREEMENT_REQUIRED`, `IDEMPOTENCY_KEY_REQUIRED`, `SLUG_TAKEN`, `SANDBOX_LIMIT`, `RETENTION`, `RESTAURANT_ARCHIVED`, `KEY_NOT_FOUND`, `KEY_IN_USE`, `WEBHOOK_NOT_FOUND`, `WEBHOOK_LIMIT`, `CHANGED`, `PDF_TOO_LARGE`, `PDF_INVALID`, `PDF_PASSWORD`, `PDF_TOO_MANY_PAGES`, `PDF_NO_TEXT`, `PDF_PARSE_TIMEOUT`, `MENU_IMPORT_EMPTY`, `FILE_UNSUPPORTED`, `IMAGE_HEIC`, `IMAGE_INVALID`, `IMAGE_DIMENSIONS`, `MENU_IMPORT_UNAVAILABLE`, `MENU_IMPORT_AI_LIMIT`, `MENU_IMPORT_TOO_BIG`, `MENU_IMPORT_NOT_FOUND`, `MENU_IMPORT_CLOSED`, `PARTNER_NOT_APPROVED`, `PARTNER_DATA_LOCKED`, `DOMAIN_NOT_FOUND`, `DOMAIN_TAKEN`, `DOMAIN_LIMIT`, `DOMAIN_MANAGED`, `DOMAIN_NOT_VERIFIED`, `OWNER_EXISTS`, `OWNER_EMAIL_NOT_ALLOWED`, `OWNER_INVITE_NOT_FOUND`, `SANDBOX_NOT_SUPPORTED`, `INVITE_SEND_FAILED`, `GO_LIVE_NOT_REQUIRED`, `GO_LIVE_NOT_READY`, `GO_LIVE_CHECKS_FAILED`, `GO_LIVE_ALREADY_REQUESTED`, `GO_LIVE_ALREADY_APPROVED`, `GO_LIVE_NOTHING_TO_WITHDRAW`, `DEMO_RESTAURANT`, `LEGAL_LOCKED`, `HOURS_OVERRIDE_NOT_FOUND`, `HOURS_OVERRIDE_LIMIT`, `DELIVERY_ZONE_NOT_FOUND`, `DELIVERY_ZONE_LIMIT`, `MENU_NOT_FOUND`, `MENU_IS_DEFAULT`, `CATEGORY_NOT_FOUND`, `PRODUCT_NOT_FOUND`, `ADDON_GROUP_NOT_FOUND`, `CATEGORY_NOT_EMPTY`, `CATEGORY_LIMIT`, `PRODUCT_LIMIT`, `ADDON_GROUP_LIMIT`, `KEY_ROLE_FORBIDDEN`, `WHITE_LABEL_REQUIRED`, `LOGO_NOT_ALLOWED`, `FILE_TOO_LARGE`, `UPLOAD_FAILED`, `PARTNER_ARCHIVED`, `PARTNER_NEWS_FORBIDDEN`, `NEWS_NOT_FOUND`, `NOT_DRAFT`, `NOT_PUBLISHED`, `NOT_SCHEDULED`, `NOT_CLAIMABLE`, `NO_RECIPIENTS`, `IMAGE_NOT_ALLOWED`, `SENDER_DOMAIN_ALREADY_SET`, `SENDER_DOMAIN_NOT_SET`, `DOMAIN_EXISTS`, `SENDER_DOMAIN_REJECTED`, `EMAIL_SERVICE_UNAVAILABLE`, `HUMAN_REQUIRED`, `LENGTH_REQUIRED`, `PAYLOAD_TOO_LARGE`
- `message` (string, required) — A human-readable explanation, often in Norwegian. For business rules it is usually suitable to show the customer.
- `requestId` (string, required) — The same value as the `X-Request-Id` response header. Search for it in the API log in LettBestilt Partner, and include it when you contact LettBestilt.
- `details` (any, optional) — Extra data for some codes, for example `issues` for `VALIDATION_FAILED`, the totals for `PRICE_CHANGED` or `requiredScope` for `INSUFFICIENT_SCOPE`.

### PartnerMenuSchedule

- `id` (string, required)
- `dayOfWeek` (enum, required)
  - Allowed values: `MONDAY`, `TUESDAY`, `WEDNESDAY`, `THURSDAY`, `FRIDAY`, `SATURDAY`, `SUNDAY`
- `startsAt` (string, required) — HH:MM (Europe/Oslo).
- `endsAt` (string, required) — HH:MM (Europe/Oslo).
- `priority` (integer, required) — Stored on the row, but not used. Which of two menus with overlapping schedule windows is shown is decided by the menu's `priority`.
- `isActive` (boolean, required)

### PartnerMenuUpdatedWarningsItems

- `path` (string, required) — Path in the request body, or an empty string for the whole menu.
- `message` (string, required)

## Examples

**Request**

```json
{
  "isActive": true,
  "isDefault": true
}
```

**Response**

```json
{
  "data": {
    "id": "string",
    "name": "string",
    "isActive": true,
    "isDefault": true,
    "isLiveNow": true,
    "priority": 1,
    "sortOrder": 1,
    "startsOn": "2023-01-15",
    "endsOn": "2023-01-15",
    "schedules": [
      {
        "id": "string",
        "dayOfWeek": "MONDAY",
        "startsAt": "string",
        "endsAt": "string",
        "priority": 1,
        "isActive": true
      }
    ],
    "categoryCount": 1,
    "productCount": 1,
    "publication": "PUBLISHED",
    "previousDefaultMenuIds": [
      "string"
    ],
    "defaultKept": true,
    "warnings": [
      {
        "path": "string",
        "message": "string"
      }
    ],
    "textCleaned": 1,
    "notice": "string"
  }
}
```

**SDK Code**

```python Partner_updatePartnerMenu_example
import requests

url = "https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId"

payload = {
    "isActive": True,
    "isDefault": True
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.patch(url, json=payload, headers=headers)

print(response.json())
```

```javascript Partner_updatePartnerMenu_example
const url = 'https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId';
const options = {
  method: 'PATCH',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"isActive":true,"isDefault":true}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Partner_updatePartnerMenu_example
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId"

	payload := strings.NewReader("{\n  \"isActive\": true,\n  \"isDefault\": true\n}")

	req, _ := http.NewRequest("PATCH", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Partner_updatePartnerMenu_example
require 'uri'
require 'net/http'

url = URI("https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Patch.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"isActive\": true,\n  \"isDefault\": true\n}"

response = http.request(request)
puts response.read_body
```

```java Partner_updatePartnerMenu_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.patch("https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"isActive\": true,\n  \"isDefault\": true\n}")
  .asString();
```

```php Partner_updatePartnerMenu_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('PATCH', 'https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId', [
  'body' => '{
  "isActive": true,
  "isDefault": true
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp Partner_updatePartnerMenu_example
using RestSharp;

var client = new RestClient("https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId");
var request = new RestRequest(Method.PATCH);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"isActive\": true,\n  \"isDefault\": true\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Partner_updatePartnerMenu_example
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "isActive": true,
  "isDefault": true
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://www.lettbestilt.no/api/v1/partner/restaurants/slug/menus/menuId")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "PATCH"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```