---
title: "Update fields of a route"
method: PATCH
path: "/api/v1/sites/{slug}/routes/{path}"
tags: ["Routes"]
---

# Update fields of a route

`PATCH /api/v1/sites/{slug}/routes/{path}`

## Request

- Base URL: `https://api.dotsy.ai`
- URL: `https://api.dotsy.ai/api/v1/sites/{slug}/routes/{path}`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer

Changes only the fields you send; an omitted field is left alone, and `null` clears a nullable field back to its default. To make a redirect, send `purpose: "redirect"` and a root-relative `redirectTo` in the same call. A redirect cannot also carry `contentKey`, `layout`, `experiment`, `nav` or `navLabel`, and `redirectTo` / `redirectStatus` need `purpose: "redirect"`. The check looks only at what you send, so a PATCH that sets `redirectTo` must repeat `purpose: "redirect"`, even on a route that is already a redirect. The home page is `…/routes/` (trailing slash). Other paths are sent without the leading slash, for example `…/routes/blog/launch` for `/blog/launch`. The change is staged to the draft — publish to make it live.

## Path parameters

- `slug` string, required
- `path` string, required

## Request body

- RouteRequest: Route fields. Everything is optional. Send `null` on a nullable field to clear it back to its default.
  - `contentKey` string, nullable: Explicit content key (defaults to a path-derived key when omitted). null clears an explicit key back to that path default — same as omitting it, but usable on a PATCH to undo a prior set.
  - `layout` string, nullable: Layout NAME, bare: "default" for layouts/default.html — not the file key. It must already exist as layouts/<name>.html (400 layout_not_found otherwise). null removes the layout.
  - `title` string, nullable
  - `vars` object, nullable
  - `purpose` string, nullable
  - `redirectTo` string, nullable: Redirect target, a root-relative path (/…). Required when purpose is "redirect". null clears it — only valid once purpose stops being "redirect" (a redirect route always needs a target).
  - `redirectStatus` union: 301 permanent / 302 or 308 temporary (default 301 on a redirect). null clears an explicit override.
    - 301
    - 302
    - 308
  - `preserveQuery` boolean, nullable: Append the visitor's original query string to the redirect target (default true on a redirect). null clears an explicit override.
  - `navLabel` string, nullable: DEPRECATED — use put_menu_item / menu API. Menu link text for <dotsy-nav> (separate from title). Still accepted during transition and synced to menu items.
  - `nav` object[], nullable: DEPRECATED — use put_menu_item / menu API. Menu memberships on the route row. Still accepted during transition and synced to menu items. See get_skill('nav').
    - `menu` string, required: Menu this page joins — matches a <dotsy-nav menu="…"> in the layout/fragment (e.g. "primary", "footer").
    - `order` integer: Sort position among siblings in this menu, ascending. Use gaps (10, 20, 30) to leave room for inserts.
    - `parent` string, nullable: Parent route path within this menu for a nested/dropdown item (e.g. "/services"). Omit for top level.
  - `experiment` object, nullable
    - `id` string, required: Stable experiment slug — cookie namespace; unique per site forever.
    - `name` string, required
    - `hypothesis` string
    - `status` 'draft' | 'running' | 'ended', required
    - `variants` object[], required
      - `id` string, required: Variant arm id — default "a" / "b" on create.
      - `label` string
      - `weight` integer, required
      - `contentKey` string, required: Published body key (may be routeless).
    - `goalId` string: Optional Dotsy conversion goal; omit when tracking in external analytics.
    - `changeNote` string: One line on what the variant changes (0.56e M2). Kept here so route PATCH round-trips (weights/settings updates) don't strip it.
    - `handsOff` boolean: Hands-off mode (0.56e M4): the daily sweep may auto-promote a ≥95% winner or auto-end keep-A after 30 days. Prefer setting this via start_test; kept here so route PATCH round-trips (weights/settings updates) don't strip the flag.
    - `startedAt` string
    - `endedAt` string
  - `noindex` boolean, nullable: Exclude from sitemap/RSS; inject noindex meta (0.69).

## Response `200`

The updated route (staged).

- RouteWriteResponse
  - `path` string, required: The URL path. The home page is `/`. A `:param` segment makes a pattern route.
  - `contentKey` string, nullable, required: The content key this route serves. Null on a redirect, and on a route with no explicit key (it then serves the key derived from its path).
  - `layout` string, nullable, required: Layout file key, for example `layouts/main.html`.
  - `title` string, nullable, required: Page title.
  - `vars` object, nullable, required: Template variables passed to the layout.
  - `purpose` string, nullable, required: `redirect` for a redirect route, otherwise null.
  - `redirectTo` string, nullable, required: Redirect target, a root-relative path. Set only when `purpose` is `redirect`.
  - `redirectStatus` union, required: Redirect status code. Null means the default (301).
    - 301
    - 302
    - 308
  - `preserveQuery` boolean, nullable, required: Whether a redirect appends the visitor's query string to its target. Null means the default (true).
  - `navLabel` string, nullable, required: Deprecated. Menu link text — use the menu API instead.
  - `nav` object[], nullable, required: Deprecated. Menu memberships — use the menu API instead.
    - `menu` string, required: Menu this page joins — matches a <dotsy-nav menu="…"> in the layout/fragment (e.g. "primary", "footer").
    - `order` integer: Sort position among siblings in this menu, ascending. Use gaps (10, 20, 30) to leave room for inserts.
    - `parent` string, nullable: Parent route path within this menu for a nested/dropdown item (e.g. "/services"). Omit for top level.
  - `experiment` object, nullable: The page's A/B test, when it has one.
    - `id` string, required: Stable experiment slug — cookie namespace; unique per site forever.
    - `name` string, required
    - `hypothesis` string
    - `status` 'draft' | 'running' | 'ended', required
    - `variants` object[], required
      - `id` string, required: Variant arm id — default "a" / "b" on create.
      - `label` string
      - `weight` integer, required
      - `contentKey` string, required: Published body key (may be routeless).
    - `goalId` string: Optional Dotsy conversion goal; omit when tracking in external analytics.
    - `changeNote` string: One line on what the variant changes (0.56e M2). Kept here so route PATCH round-trips (weights/settings updates) don't strip it.
    - `handsOff` boolean: Hands-off mode (0.56e M4): the daily sweep may auto-promote a ≥95% winner or auto-end keep-A after 30 days. Prefer setting this via start_test; kept here so route PATCH round-trips (weights/settings updates) don't strip the flag.
    - `startedAt` string
    - `endedAt` string
  - `noindex` boolean, nullable: True when the page is excluded from the sitemap and marked `noindex`.
  - `createdBy` 'person' | 'ai' | 'key' | 'move', nullable: Who made this route a redirect: a signed-in `person`, an `ai` (MCP OAuth), an API `key`, or a page `move`. Present only on redirects.
  - `createdById` string, nullable: The user id when `createdBy` is `person`.
  - `updatedAt` number, required: Last change, Unix ms.
  - `created` boolean: `PUT` only: true when the route did not exist before.
  - `published` false, required: Always false: a route change is staged until you publish.
  - `hint` string, required

## Other responses

- `400`: The body is not valid (`invalid_body`), the path is platform-owned (`reserved_route`), the content key is reserved (`reserved_content_key`), or the layout file does not exist (`layout_not_found`).
- `401`: Missing or invalid credentials.
- `403`: The key or token cannot access this site (viewer role for a write, or a key scoped to other sites).
- `404`: No such site (`site_not_found`), or no route at that path (`route_not_found`).
- `422`: A route experiment or redirect field is not valid. `error` names the rule, and `field` the offending field.

---

[API](https://skmtc.dev/dotsy/apis/dotsy-api.md) · [All operations](https://skmtc.dev/dotsy/apis/dotsy-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dotsy/apis/dotsy-api/revisions/4272ed3b85d7?raw)
