> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shftd2.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Replace all routing rules

> **Replaces every rule of the site** with the list sent. Rules missing from the list are deleted; `{"routes": []}` deletes them all. Read the current rules first.

The whole list is checked first — at most 200 rules, each path and match mode, each reference (a fragment of this site; a paused one is accepted), duplicates after canonicalization — then applied at once: on any error nothing changes. Every rule gets a new id; site and fragment ids never change.

The replacement is atomic within this request only. Nothing protects it from another client replacing the rules at the same time: the last write wins.



## OpenAPI

````yaml /openapi-management.json put /v1/sites/{domain}/routes
openapi: 3.1.0
info:
  title: doubleshift management API
  version: v1
  description: >-
    The management API writes the objects a publisher organisation is made of —
    sites, fragments, the site default, routing rules, card eligibility and
    delivery API keys — with the same rules and the same error messages as the
    console. It is a separate contract from the fragment endpoint
    (`openapi.json`): another credential, and no card serving. `info.version`
    names the contract (`/v1`), not a console release. Guide:
    https://docs.shftd2.com/management-api


    **Host.** `https://api.shftd2.com` only, paths under `/v1`. The console host
    `app.shftd2.com` answers `404` on them.


    **Authentication.** `Authorization: Bearer ds_mgmt_…`: a management token,
    created by an owner in the console (**Settings → Management tokens**) and
    shown once. A token acts on its whole organisation. Cookies are never read.
    A delivery API key (`ds_live_…`) is refused here, and a management token is
    refused by the fragment endpoint.


    **Requests.** Every PUT, POST and DELETE sends `Content-Type:
    application/json` (parameters such as `charset` allowed) and a JSON object —
    `{}` for a DELETE. The body is capped at 262,144 bytes, counted on the raw
    bytes (a UTF-8 byte order mark included) before any parsing: over the cap,
    `400 body_too_large`. Unknown fields are refused (`400 unknown_field`),
    nested ones included. A GET takes no body.


    **Responses.** JSON, with `Cache-Control: private, no-store` and
    `RateLimit-Policy` on every response, success or error. Lists are `{"items":
    [...]}` and complete: there is no pagination. Errors are `{"error": {"code",
    "message", "field"?, "retry_after"?}}`. A resource that does not exist and
    one that belongs to another organisation get the same `404`.


    **Order of checks.** Token and limits (`401`, `429`); then the body: media
    type, size, JSON, unknown fields (`400`), then its shape (`422`); then the
    addressed site, card or key (`404`); then what depends on stored data
    (`403`, `409`, `422`). Nothing is written before every check has passed.


    **Limits.** Per token: 120 requests per minute, of which at most 60 writes
    (PUT, POST, DELETE). Per client IP address: 30 failed authentications per
    minute (no token, or a malformed, unknown, revoked or expired one), and 600
    requests per minute presenting a well-formed token. Over a limit: `429
    rate_limited` with `Retry-After: 60`, and nothing is written. The counters
    are kept per Cloudflare location, so the limits are approximate, and no
    remaining count is sent.
servers:
  - url: https://api.shftd2.com
    description: >-
      The machine host. The management API exists on this host only; there is no
      alias.
security:
  - bearerAuth: []
tags:
  - name: Account
    description: The organisation a token acts on.
  - name: Sites
    description: Sites, addressed by domain.
  - name: Fragments
    description: Fragments, addressed by site domain and reference.
  - name: Site default
    description: The fragment a page resolves to when no routing rule matches.
  - name: Routing rules
    description: Paths that resolve to a fragment of the site.
  - name: Cards
    description: Cards, read-only through this API.
  - name: Eligibility
    description: >-
      The fragments a card is eligible on. Several cards can be eligible on the
      same fragment.
  - name: API keys
    description: Delivery API keys for the fragment endpoint.
paths:
  /v1/sites/{domain}/routes:
    parameters:
      - $ref: '#/components/parameters/Domain'
    put:
      tags:
        - Routing rules
      summary: Replace all routing rules
      description: >-
        **Replaces every rule of the site** with the list sent. Rules missing
        from the list are deleted; `{"routes": []}` deletes them all. Read the
        current rules first.


        The whole list is checked first — at most 200 rules, each path and match
        mode, each reference (a fragment of this site; a paused one is
        accepted), duplicates after canonicalization — then applied at once: on
        any error nothing changes. Every rule gets a new id; site and fragment
        ids never change.


        The replacement is atomic within this request only. Nothing protects it
        from another client replacing the rules at the same time: the last write
        wins.
      operationId: replaceRoutes
      requestBody:
        required: true
        description: >-
          Send `Content-Type: application/json` and a JSON object of at most
          262,144 bytes.
        x-max-body-bytes: 262144
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoutesReplace'
      responses:
        '200':
          description: The site's rules after the replacement.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No site of this organisation has this domain.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: not found
        '409':
          description: >-
            The site holds a rule this replacement could not remove. Not
            expected in normal use; nothing was changed.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                conflict:
                  value:
                    error:
                      code: conflict
                      message: >-
                        A rule with this path and match already exists on this
                        site
                      field: routes
        '422':
          description: >-
            A value was refused; `field` names it: `body`, `routes` (missing,
            not an array, or more than 200 rules), `routes[i]` (not an object),
            `routes[i].path_prefix` (missing, invalid, too long, or a duplicate
            of an earlier rule after canonicalization), `routes[i].match`,
            `routes[i].ref` (not a fragment of this site).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                duplicate:
                  value:
                    error:
                      code: validation
                      message: >-
                        Duplicate rule: /politics/ (prefix) appears twice in the
                        list
                      field: routes[1].path_prefix
                ref:
                  value:
                    error:
                      code: validation
                      message: Choose a fragment of this site
                      field: routes[0].ref
                ceiling:
                  value:
                    error:
                      code: validation
                      message: At most 200 routing rules per site
                      field: routes
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
      x-codeSamples:
        - lang: bash
          label: curl
          source: >-
            curl -sS -X PUT
            "https://api.shftd2.com/v1/sites/news-site.example/routes" \
              -H "Authorization: Bearer $DS_MANAGEMENT_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"routes":[{"path_prefix":"/politics/","match":"prefix","ref":"/1234567/news-site/organic/politics"}]}'
components:
  parameters:
    Domain:
      name: domain
      in: path
      required: true
      description: >-
        The site's bare domain, normalized like the console form (trimmed,
        lowercase). `www.news-site.example` and `news-site.example` are two
        different sites. A domain that is not a site of this organisation —
        absent, another organisation's, or not a valid domain — is `404`, except
        on `PUT /v1/sites/{domain}`, which creates the site and answers `422`
        for an invalid domain.
      schema:
        type: string
      example: news-site.example
  schemas:
    RoutesReplace:
      type: object
      additionalProperties: false
      required:
        - routes
      properties:
        routes:
          type: array
          maxItems: 200
          description: >-
            The complete list of the site's rules. `[]` deletes them all. The
            same path and match mode may appear once, compared after
            canonicalization.
          items:
            $ref: '#/components/schemas/RouteWrite'
      example:
        routes:
          - path_prefix: /politics/
            match: prefix
            ref: /1234567/news-site/organic/politics
          - path_prefix: /sports/
            match: prefix
            ref: /1234567/news-site/organic/sports
    RouteList:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Route'
      example:
        items:
          - id: 00000000-0000-4000-8000-000000000001
            path_prefix: /politics/
            match: prefix
            fragment_ref: /1234567/news-site/organic/politics
            fragment_id: frg_examplefr1
          - id: 00000000-0000-4000-8000-000000000002
            path_prefix: /sports/
            match: prefix
            fragment_ref: /1234567/news-site/organic/sports
            fragment_id: frg_examplefr2
    Error:
      type: object
      description: >-
        Every error of this API. Like every response, it carries `Cache-Control:
        private, no-store` and `RateLimit-Policy: "token";q=120;w=60,
        "write";q=60;w=60`.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - unsupported_media_type
                - body_too_large
                - invalid_json
                - unknown_field
                - missing_token
                - invalid_token
                - platform_card
                - not_found
                - conflict
                - validation
                - rate_limited
                - internal
              description: Stable, machine-readable code. Branch on it, not on `message`.
            message:
              type: string
              description: >-
                Human-readable explanation, the console wording where the
                console has one. May change.
            field:
              type: string
              description: >-
                The field at fault, nested ones included
                (`routes[3].path_prefix`, `fragments[0].ref`). Present on
                `unknown_field`, `validation` and `conflict`; absent otherwise.
            retry_after:
              type: integer
              const: 60
              description: Seconds to wait. Present on `rate_limited` only.
      example:
        error:
          code: validation
          message: Choose a fragment of this site
          field: routes[0].ref
    RouteWrite:
      type: object
      additionalProperties: false
      required:
        - path_prefix
        - match
        - ref
      properties:
        path_prefix:
          type: string
          description: >-
            A path. Surrounding whitespace is trimmed first; the trimmed path is
            at most 512 characters (UTF-16 code units), and its stored canonical
            form at most 200: lowercase, wrapped in `/`, with canonical
            percent-encoding. No query, hash, inner whitespace, backslash, dot
            segment, repeated slash or invalid percent-encoding. Anything else
            is refused with `422`.
        match:
          type: string
          description: >-
            `exact` or `prefix`. Surrounding whitespace is ignored; any other
            value is refused with `422`. Stored and returned without the
            whitespace.
          examples:
            - prefix
            - exact
        ref:
          type: string
          pattern: ^[A-Za-z0-9/_.:-]{1,200}$
          description: >-
            The reference of a fragment of this site — a paused one is accepted
            — as a plain JSON string, exact and case-sensitive: it is not
            trimmed.
      example:
        path_prefix: /politics/
        match: prefix
        ref: /1234567/news-site/organic/politics
    Route:
      type: object
      description: >-
        A routing rule of a site: a path and a match mode that resolve to one
        fragment of the site.
      required:
        - id
        - path_prefix
        - match
        - fragment_ref
        - fragment_id
      properties:
        id:
          type: string
          description: The rule id. `PUT …/routes` gives every rule a new id.
        path_prefix:
          type: string
          description: >-
            The canonical path: lowercase, starting and ending with `/`, each
            segment percent-encoded (`Actualite/` is stored as `/actualite/`).
        match:
          type: string
          enum:
            - exact
            - prefix
        fragment_ref:
          type:
            - string
            - 'null'
          description: >-
            The target fragment reference, or null when that fragment has no
            reference.
        fragment_id:
          type: string
          description: The target fragment id (`frg_…`).
      example:
        id: 00000000-0000-4000-8000-000000000001
        path_prefix: /politics/
        match: prefix
        fragment_ref: /1234567/news-site/organic/politics
        fragment_id: frg_examplefr1
  headers:
    CacheControl:
      description: On every response of this API.
      schema:
        type: string
        const: private, no-store
    RateLimitPolicy:
      description: >-
        On every response of this API: 120 requests and 60 writes per 60 seconds
        per token (IETF draft ratelimit-headers syntax). No remaining count is
        sent.
      schema:
        type: string
        const: '"token";q=120;w=60, "write";q=60;w=60'
    WWWAuthenticate:
      description: On every 401.
      schema:
        type: string
        const: Bearer realm="doubleshift"
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        const: 60
  responses:
    BadRequest:
      description: >-
        The body was refused before its values were read:
        `unsupported_media_type` (no `Content-Type: application/json`),
        `body_too_large` (over 262,144 bytes, counted before parsing),
        `invalid_json`, or `unknown_field` (a field this operation does not
        accept, nested ones included; `field` names it).
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unsupported_media_type:
              value:
                error:
                  code: unsupported_media_type
                  message: 'send the body as JSON, with "Content-Type: application/json"'
            body_too_large:
              value:
                error:
                  code: body_too_large
                  message: request body over 262144 bytes
            invalid_json:
              value:
                error:
                  code: invalid_json
                  message: request body is not valid JSON
            unknown_field:
              value:
                error:
                  code: unknown_field
                  message: unknown field
                  field: domain
    Unauthorized:
      description: >-
        `missing_token`: no `Authorization: Bearer` header. `invalid_token`:
        anything else that is not a live management token of an organisation —
        malformed, unknown, revoked or expired, or a delivery API key
        (`ds_live_…`). Cookies are never read. Carries `WWW-Authenticate: Bearer
        realm="doubleshift"`.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        WWW-Authenticate:
          $ref: '#/components/headers/WWWAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing_token:
              value:
                error:
                  code: missing_token
                  message: >-
                    missing management token — send "Authorization: Bearer
                    ds_mgmt_…"
            invalid_token:
              value:
                error:
                  code: invalid_token
                  message: invalid management token
    RateLimited:
      description: >-
        A limit was reached (per token: 120 requests or 60 writes per minute;
        per IP address: 30 failed authentications or 600 requests presenting a
        well-formed token per minute). Nothing was written. Carries
        `Retry-After: 60`, and `retry_after: 60` in the body: wait that long
        before retrying.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: rate limited — retry after 60 seconds
              retry_after: 60
    Internal:
      description: An unexpected server error. The message is fixed and carries no detail.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            internal:
              value:
                error:
                  code: internal
                  message: internal error
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        A management token: `ds_mgmt_` followed by 40 lowercase hexadecimal
        characters, created by an owner in **Settings → Management tokens**.
        Opaque: send it as is, in the `Authorization` header only.

````