> ## 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 a card's eligibility

> **Replaces the card's eligibility in this organisation** with the list sent. Fragments missing from the list lose the card. `{"fragments": []}` empties the list, and the card becomes eligible everywhere in the organisation. Several cards can be eligible on the same fragment. Read the current list first.

Each fragment is addressed by its site domain and its reference; only this organisation's fragments are accepted (a paused one too), and a card in any status can be made eligible. The list is checked entirely first, then applied at once: on any error nothing changes. The replacement is atomic within this request only; nothing protects it from a concurrent replacement.

Order of checks: the body (`400`, then `422` on its shape and on each domain and reference), then the card (`404`), then platform cards (`403`), then whether each fragment exists in this organisation (`422`).



## OpenAPI

````yaml /openapi-management.json put /v1/cards/{id}/eligibility
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/cards/{id}/eligibility:
    parameters:
      - $ref: '#/components/parameters/CardId'
    put:
      tags:
        - Eligibility
      summary: Replace a card's eligibility
      description: >-
        **Replaces the card's eligibility in this organisation** with the list
        sent. Fragments missing from the list lose the card. `{"fragments": []}`
        empties the list, and the card becomes eligible everywhere in the
        organisation. Several cards can be eligible on the same fragment. Read
        the current list first.


        Each fragment is addressed by its site domain and its reference; only
        this organisation's fragments are accepted (a paused one too), and a
        card in any status can be made eligible. The list is checked entirely
        first, then applied at once: on any error nothing changes. The
        replacement is atomic within this request only; nothing protects it from
        a concurrent replacement.


        Order of checks: the body (`400`, then `422` on its shape and on each
        domain and reference), then the card (`404`), then platform cards
        (`403`), then whether each fragment exists in this organisation (`422`).
      operationId: replaceCardEligibility
      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/EligibilityReplace'
      responses:
        '200':
          description: The card's eligibility after the replacement.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EligibilityList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            A platform card (`account_id: null`): doubleshift manages it, and no
            management token can change its eligibility.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                platform_card:
                  value:
                    error:
                      code: platform_card
                      message: >-
                        This platform card is managed by doubleshift and is
                        read-only.
        '404':
          description: No card of this organisation, nor platform card, has this id.
          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
        '422':
          description: >-
            A value was refused; `field` names it: `body`, `fragments` (missing
            or not an array), `fragments[i]` (not an object),
            `fragments[i].domain` (missing, not a string, or not a valid
            domain), `fragments[i].ref` (missing, not a string, not a valid
            reference, or not a fragment of this organisation).
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                target:
                  value:
                    error:
                      code: validation
                      message: Choose fragments of this account
                      field: fragments[0].ref
                domain:
                  value:
                    error:
                      code: validation
                      message: Enter a valid domain, e.g. example.com
                      field: fragments[0].domain
        '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/cards/crd_examplecard1/eligibility" \
              -H "Authorization: Bearer $DS_MANAGEMENT_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"fragments":[{"domain":"news-site.example","ref":"/1234567/news-site/organic/politics"}]}'
components:
  parameters:
    CardId:
      name: id
      in: path
      required: true
      description: The card id (`crd_…`), from `GET /v1/cards`.
      schema:
        type: string
      example: crd_examplecard1
  schemas:
    EligibilityReplace:
      type: object
      additionalProperties: false
      required:
        - fragments
      properties:
        fragments:
          type: array
          description: >-
            The complete list of the fragments of this organisation the card is
            eligible on. `[]` empties it: the card is then eligible everywhere
            in the organisation. The same fragment named twice counts once.
          items:
            $ref: '#/components/schemas/EligibilityTarget'
      example:
        fragments:
          - domain: news-site.example
            ref: /1234567/news-site/organic/politics
    EligibilityList:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Eligibility'
      example:
        items:
          - card_id: crd_examplecard1
            fragment_id: frg_examplefr1
            domain: news-site.example
            ref: /1234567/news-site/organic/politics
    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
    EligibilityTarget:
      type: object
      additionalProperties: false
      required:
        - domain
        - ref
      properties:
        domain:
          type: string
          description: >-
            The site domain, normalized like the console form: trimmed,
            lowercased, a scheme, path or port dropped; it must then be a valid
            public domain (`422` otherwise).
        ref:
          type: string
          pattern: ^[A-Za-z0-9/_.:-]{1,200}$
          description: >-
            The fragment reference, exact and case-sensitive (not trimmed), as a
            plain JSON string.
      example:
        domain: news-site.example
        ref: /1234567/news-site/organic/politics
    Eligibility:
      type: object
      description: >-
        One fragment of this organisation a card is eligible on. Several cards
        can be eligible on the same fragment.
      required:
        - card_id
        - fragment_id
        - domain
        - ref
      properties:
        card_id:
          type: string
        fragment_id:
          type: string
        domain:
          type: string
          description: The fragment site domain.
        ref:
          type:
            - string
            - 'null'
          description: The fragment reference, or null when the fragment has none.
      example:
        card_id: crd_examplecard1
        fragment_id: frg_examplefr1
        domain: news-site.example
        ref: /1234567/news-site/organic/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.

````