> ## 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.

# Create or update a fragment

> Creates the fragment with this reference on the site (`201`, `active` unless `status` says otherwise), or updates it (`200`). The reference is the address: it never changes here. Sending the same body again leaves the same state and answers `200`.

The body describes the **whole** fragment. On an update, a field left out is reset: `lang` to `fr`, `description` to none, `key_values` to `{}`. Only `status` keeps its current value when absent.

Pausing a fragment keeps the site default and the rules that point to it: their pages answer `204`. `warnings` lists them. There is no DELETE: deleting a fragment is a console action; pause it instead.

The body is checked before the site is looked up: an invalid body on an unknown site is a `422`, not a `404`.



## OpenAPI

````yaml /openapi-management.json put /v1/sites/{domain}/fragments/{ref}
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}/fragments/{ref}:
    parameters:
      - $ref: '#/components/parameters/Domain'
      - $ref: '#/components/parameters/Ref'
    put:
      tags:
        - Fragments
      summary: Create or update a fragment
      description: >-
        Creates the fragment with this reference on the site (`201`, `active`
        unless `status` says otherwise), or updates it (`200`). The reference is
        the address: it never changes here. Sending the same body again leaves
        the same state and answers `200`.


        The body describes the **whole** fragment. On an update, a field left
        out is reset: `lang` to `fr`, `description` to none, `key_values` to
        `{}`. Only `status` keeps its current value when absent.


        Pausing a fragment keeps the site default and the rules that point to
        it: their pages answer `204`. `warnings` lists them. There is no DELETE:
        deleting a fragment is a console action; pause it instead.


        The body is checked before the site is looked up: an invalid body on an
        unknown site is a `422`, not a `404`.
      operationId: putFragment
      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/FragmentWrite'
      responses:
        '200':
          description: The fragment existed and was updated (or left unchanged).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FragmentWithWarnings'
        '201':
          description: The fragment was created.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FragmentWithWarnings'
              examples:
                created:
                  value:
                    id: frg_examplefr1
                    site_id: sit_examplesite1
                    domain: news-site.example
                    ref: /1234567/news-site/organic/politics
                    name: Politics
                    description: null
                    lang: en
                    key_values:
                      section:
                        - politics
                    status: active
                    created_at: '2026-09-26T09:00:00.000Z'
                    warnings: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >-
            No site of this organisation has this domain (or the fragment was
            deleted in the console while this call ran).
          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: >-
            A concurrent write on this reference could not be completed. Read
            the fragments, then retry.
          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: Reference already used by another fragment of this site
                      field: ref
        '422':
          description: >-
            A value was refused; `field` names it: `body`, `name` (missing, not
            a string, or blank), `status` (not `active` or `paused`), `lang`
            (not a supported language), `description` (not a string), `ref` (the
            decoded path segment is not a valid reference — a double-encoded one
            included), `key_values`.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                name:
                  value:
                    error:
                      code: validation
                      message: Fragment name is required
                      field: name
                ref:
                  value:
                    error:
                      code: validation
                      message: 'Reference: 1-200 characters, A-Z a-z 0-9 / _ - . : only'
                      field: ref
                ref_dot_segment:
                  value:
                    error:
                      code: validation
                      message: a reference cannot contain a "." or ".." path segment
                      field: ref
                key_values:
                  value:
                    error:
                      code: validation
                      message: >-
                        Key "section": each value is a non-empty string without
                        commas or line breaks
                      field: key_values
        '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/fragments/%2F1234567%2Fnews-site%2Forganic%2Fpolitics"
            \
              -H "Authorization: Bearer $DS_MANAGEMENT_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"name":"Politics","lang":"en","key_values":{"section":["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
    Ref:
      name: ref
      in: path
      required: true
      description: >-
        The fragment reference, sent as **one** path segment: encode the whole
        reference once with `encodeURIComponent`, so every `/` travels as `%2F`
        (`/1234567/news-site/organic/politics` →
        `%2F1234567%2Fnews-site%2Forganic%2Fpolitics`). Unencoded slashes match
        no operation (`404`); a double-encoded reference (`%252F…`) is refused
        (`422`). A reference is case-sensitive: 1 to 200 characters among `A-Z
        a-z 0-9 / _ - . :`, and no path segment may be `.` or `..` on its own
        (URL processing would remove it). The value below is the decoded
        reference.
      schema:
        type: string
        pattern: ^[A-Za-z0-9/_.:-]{1,200}$
      example: /1234567/news-site/organic/politics
  schemas:
    FragmentWrite:
      type: object
      additionalProperties: false
      required:
        - name
      description: >-
        The fragment, as a whole: on an update, `lang`, `description` and
        `key_values` are reset to their default when absent. Only `status` keeps
        its current value when absent.
      properties:
        name:
          type: string
          minLength: 1
          description: Required. Trimmed, and must not be blank once trimmed.
        status:
          type: string
          enum:
            - active
            - paused
          description: >-
            Exact value, not trimmed. Absent: `active` on creation, unchanged on
            update.
        lang:
          type: string
          enum:
            - fr
            - en
            - de
            - es
            - it
          default: fr
          description: >-
            Exact value, not trimmed. Absent: `fr`, on an update too — send it
            every time.
        description:
          type: string
          description: 'Trimmed. Absent, empty or blank: no description (null).'
        key_values:
          $ref: '#/components/schemas/KeyValues'
      example:
        name: Politics
        lang: en
        key_values:
          section:
            - politics
    FragmentWithWarnings:
      type: object
      description: A Fragment, plus what a paused fragment still serves.
      required:
        - id
        - site_id
        - domain
        - ref
        - name
        - description
        - lang
        - key_values
        - status
        - created_at
        - warnings
      properties:
        id:
          type: string
          description: Stable fragment id (`frg_…`). Never changes.
        site_id:
          type: string
          description: The site id (`sit_…`).
        domain:
          type: string
          description: The site domain.
        ref:
          type:
            - string
            - 'null'
          description: >-
            The fragment reference. Null for a fragment created in the console
            without one: it is listed, but this API cannot write it, target it
            with a rule, or make a card eligible on it.
        name:
          type: string
          description: The fragment name.
        description:
          type:
            - string
            - 'null'
          description: The description, or null.
        lang:
          type: string
          description: 'The fragment language: `fr`, `en`, `de`, `es` or `it`.'
        key_values:
          $ref: '#/components/schemas/KeyValues'
        status:
          type: string
          enum:
            - active
            - paused
          description: >-
            `paused`: every page that resolves to this fragment answers `204` on
            the fragment endpoint.
        created_at:
          type: string
          format: date-time
          description: Creation time, UTC.
        warnings:
          type: array
          items:
            type: string
          description: >-
            For a paused fragment, what still resolves to it and therefore
            answers `204`: `"site default"`, `"target of N rules"` (`"target of
            1 rule"`). `[]` for an active fragment, or a paused one nothing
            resolves to.
      example:
        id: frg_examplefr2
        site_id: sit_examplesite1
        domain: news-site.example
        ref: /1234567/news-site/organic/sports
        name: Sports
        description: Sports section
        lang: en
        key_values: {}
        status: paused
        created_at: '2026-09-26T09:00:00.000Z'
        warnings:
          - site default
          - target of 2 rules
    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
    KeyValues:
      type: object
      description: >-
        Legacy targeting key-values, `{"key": ["value", …]}`; `{}` when none.
        Keys match `^[a-z0-9_-]+$` exactly (not trimmed; `__proto__` is
        refused). Each key has a non-empty list of strings; each value is
        trimmed, and must then be non-empty and without commas or line breaks.
      propertyNames:
        pattern: ^[a-z0-9_-]+$
        not:
          const: __proto__
      additionalProperties:
        type: array
        minItems: 1
        items:
          type: string
          minLength: 1
      example:
        section:
          - politics
  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.

````