{
  "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\n\n**Host.** `https://api.shftd2.com` only, paths under `/v1`. The console host `app.shftd2.com` answers `404` on them.\n\n**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.\n\n**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.\n\n**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`.\n\n**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.\n\n**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/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Get the token's organisation",
        "description": "The organisation this token writes to, and the token id. Call it first: a script checks the organisation id before any write. Changes nothing.",
        "responses": {
          "200": {
            "description": "The organisation and the token.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The token or its organisation was removed 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"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/me\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      }
    },
    "/v1/sites": {
      "get": {
        "operationId": "listSites",
        "tags": [
          "Sites"
        ],
        "summary": "List sites",
        "description": "Every site of the organisation, in byte order of domain. Complete: no pagination.",
        "responses": {
          "200": {
            "description": "The sites.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/sites\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      }
    },
    "/v1/sites/{domain}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Domain"
        }
      ],
      "get": {
        "operationId": "getSite",
        "tags": [
          "Sites"
        ],
        "summary": "Get a site",
        "description": "One site of the organisation, by domain.",
        "responses": {
          "200": {
            "description": "The site.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/sites/news-site.example\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      },
      "put": {
        "operationId": "putSite",
        "tags": [
          "Sites"
        ],
        "summary": "Create or update a site",
        "description": "Creates the site with this domain in the organisation (`201`), or updates it (`200`). Sending the same body again leaves the same state and answers `200`.\n\n- A site is created in **your** organisation even if another organisation registered the same domain.\n- `name` absent keeps the current name; `\"\"` clears it.\n- `default_fragment_ref` absent keeps the current default. To set a default on a new site, create the site, then its fragments, then call this again (or `PUT …/default-fragment`).\n- A site created through this API has no icon (`favicon_url: null`) until it is first opened in the console.\n- The domain cannot be changed. There is no DELETE: deleting a site is a console action.",
        "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/SiteWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site 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/Site"
                },
                "examples": {
                  "updated": {
                    "value": {
                      "id": "sit_examplesite1",
                      "account_id": "acc_example1",
                      "domain": "news-site.example",
                      "name": "News site",
                      "favicon_url": null,
                      "default_fragment_id": "frg_examplefr1",
                      "default_fragment_ref": "/1234567/news-site/organic/politics",
                      "created_at": "2026-09-26T09:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The site was created.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "examples": {
                  "created": {
                    "value": {
                      "id": "sit_examplesite1",
                      "account_id": "acc_example1",
                      "domain": "news-site.example",
                      "name": "News site",
                      "favicon_url": null,
                      "default_fragment_id": null,
                      "default_fragment_ref": null,
                      "created_at": "2026-09-26T09:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The site 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 creation of this site could not be completed. Read the site, 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": "news-site.example is already registered on this account",
                        "field": "domain"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "A value was refused; `field` names it: `body` (not a JSON object), `domain` (the path is not a valid domain), `name` (not a string), `default_fragment_ref` (not a string, or not an active fragment of this site — always refused on creation).",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default_fragment_ref": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "Choose an active fragment of this site as the default",
                        "field": "default_fragment_ref"
                      }
                    }
                  },
                  "domain": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "Enter a valid domain, e.g. example.com",
                        "field": "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/sites/news-site.example\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"News site\"}'"
          }
        ]
      }
    },
    "/v1/sites/{domain}/fragments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Domain"
        }
      ],
      "get": {
        "operationId": "listFragments",
        "tags": [
          "Fragments"
        ],
        "summary": "List a site's fragments",
        "description": "Every fragment of the site, active and paused: references in byte order (uppercase before lowercase), fragments without a reference last, then by id. Complete: no pagination.",
        "responses": {
          "200": {
            "description": "The fragments.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentList"
                }
              }
            }
          },
          "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/sites/news-site.example/fragments\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      }
    },
    "/v1/sites/{domain}/fragments/{ref}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Domain"
        },
        {
          "$ref": "#/components/parameters/Ref"
        }
      ],
      "put": {
        "operationId": "putFragment",
        "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`.\n\nThe 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.\n\nPausing 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.\n\nThe body is checked before the site is looked up: an invalid body on an unknown site is a `422`, not a `404`.",
        "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\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Politics\",\"lang\":\"en\",\"key_values\":{\"section\":[\"politics\"]}}'"
          }
        ]
      }
    },
    "/v1/sites/{domain}/default-fragment": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Domain"
        }
      ],
      "put": {
        "operationId": "putDefaultFragment",
        "tags": [
          "Site default"
        ],
        "summary": "Set the default fragment",
        "description": "Sets the site default: the fragment a page resolves to when no routing rule matches. It must be an **active** fragment of this site. Repeatable: the same body answers `200` with the same state.",
        "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/DefaultFragmentWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site, with its new default.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`body` (not a JSON object), or `ref`: missing, not a string, or not an active 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": {
                  "ref": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "Choose an active fragment of this site as the default",
                        "field": "ref"
                      }
                    }
                  }
                }
              }
            }
          },
          "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/default-fragment\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"ref\":\"/1234567/news-site/organic/politics\"}'"
          }
        ]
      },
      "delete": {
        "operationId": "deleteDefaultFragment",
        "tags": [
          "Site default"
        ],
        "summary": "Clear the default fragment",
        "description": "Removes the site default: pages no rule matches then answer `204`. The fragment itself is kept. Repeatable: a site without default answers `200` too. The body is `{}`.",
        "requestBody": {
          "required": true,
          "description": "Send `Content-Type: application/json` and `{}`.",
          "x-max-body-bytes": 262144,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmptyBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site, without default.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "examples": {
                  "cleared": {
                    "value": {
                      "id": "sit_examplesite1",
                      "account_id": "acc_example1",
                      "domain": "news-site.example",
                      "name": "News site",
                      "favicon_url": null,
                      "default_fragment_id": null,
                      "default_fragment_ref": null,
                      "created_at": "2026-09-26T09:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`body`: not a JSON object.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "body": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "the request body must be a JSON object",
                        "field": "body"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS -X DELETE \"https://api.shftd2.com/v1/sites/news-site.example/default-fragment\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'"
          }
        ]
      }
    },
    "/v1/sites/{domain}/routes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Domain"
        }
      ],
      "get": {
        "operationId": "listRoutes",
        "tags": [
          "Routing rules"
        ],
        "summary": "List a site's routing rules",
        "description": "Every routing rule of the site, in byte order of path, then match mode. Complete: no pagination.",
        "responses": {
          "200": {
            "description": "The rules.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteList"
                }
              }
            }
          },
          "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/sites/news-site.example/routes\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      },
      "put": {
        "operationId": "replaceRoutes",
        "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.\n\nThe 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.\n\nThe replacement is atomic within this request only. Nothing protects it from another client replacing the rules at the same time: the last write wins.",
        "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\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"routes\":[{\"path_prefix\":\"/politics/\",\"match\":\"prefix\",\"ref\":\"/1234567/news-site/organic/politics\"}]}'"
          }
        ]
      },
      "post": {
        "operationId": "addRoute",
        "tags": [
          "Routing rules"
        ],
        "summary": "Add a routing rule",
        "description": "Adds one rule to the site, targeting a fragment of this site (a paused one is accepted). Not repeatable: the same rule again is a `409`. A site holds at most 200 rules.",
        "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/RouteWrite"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The rule was added.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                }
              }
            }
          },
          "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": "A rule with the same canonical path and match mode already exists on the site.",
            "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": "path_prefix"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "A value was refused; `field` names it: `body`, `path_prefix`, `match`, `ref` (missing, not a string, invalid, or not a fragment of this site), `routes` (the site already has 200 rules).",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "path_prefix": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "Enter a valid path prefix: no query, hash, whitespace, dot segments, repeated slashes or invalid percent-encoding",
                        "field": "path_prefix"
                      }
                    }
                  },
                  "ceiling": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "This site already has 200 routing rules — delete one before adding another",
                        "field": "routes"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS -X POST \"https://api.shftd2.com/v1/sites/news-site.example/routes\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"path_prefix\":\"/politics/\",\"match\":\"prefix\",\"ref\":\"/1234567/news-site/organic/politics\"}'"
          }
        ]
      }
    },
    "/v1/sites/{domain}/routes/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Domain"
        },
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "delete": {
        "operationId": "deleteRoute",
        "tags": [
          "Routing rules"
        ],
        "summary": "Delete a routing rule",
        "description": "Deletes one rule of the site. Not repeatable: once deleted, the same id is a `404`. The body is `{}`.",
        "requestBody": {
          "required": true,
          "description": "Send `Content-Type: application/json` and `{}`.",
          "x-max-body-bytes": 262144,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmptyBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rule was deleted.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuleDeleted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No site of this organisation has this domain, or the site has no rule with this id — already deleted included.",
            "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": "`body`: not a JSON object.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "body": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "the request body must be a JSON object",
                        "field": "body"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS -X DELETE \"https://api.shftd2.com/v1/sites/news-site.example/routes/00000000-0000-4000-8000-000000000001\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'"
          }
        ]
      }
    },
    "/v1/cards": {
      "get": {
        "operationId": "listCards",
        "tags": [
          "Cards"
        ],
        "summary": "List cards",
        "description": "The organisation's own cards and the platform cards (`account_id: null`), in every status (`draft`, `active`, `archived`), in byte order of id. Complete: no pagination. Cards are created, edited and activated in the console; this API only lists them and sets their eligibility.",
        "responses": {
          "200": {
            "description": "The cards.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/cards\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      }
    },
    "/v1/cards/{id}/eligibility": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "get": {
        "operationId": "listCardEligibility",
        "tags": [
          "Eligibility"
        ],
        "summary": "List a card's eligibility",
        "description": "The fragments of this organisation the card is eligible on, by domain, then reference (fragments without one last), then fragment id. For one of this organisation's cards, an empty list means it is eligible everywhere in the organisation. For a platform card, the eligibility other organisations set is never shown, so an empty list is not conclusive: the card may be eligible everywhere, or only on their fragments.",
        "responses": {
          "200": {
            "description": "The card's eligibility.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EligibilityList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/cards/crd_examplecard1/eligibility\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      },
      "put": {
        "operationId": "replaceCardEligibility",
        "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.\n\nEach 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.\n\nOrder 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`).",
        "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\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"fragments\":[{\"domain\":\"news-site.example\",\"ref\":\"/1234567/news-site/organic/politics\"}]}'"
          }
        ]
      }
    },
    "/v1/keys": {
      "get": {
        "operationId": "listKeys",
        "tags": [
          "API keys"
        ],
        "summary": "List API keys",
        "description": "Every delivery API key of the organisation, revoked ones included, newest first, then by id. Never the full key. Complete: no pagination.",
        "responses": {
          "200": {
            "description": "The keys.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS \"https://api.shftd2.com/v1/keys\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\""
          }
        ]
      },
      "post": {
        "operationId": "createKey",
        "tags": [
          "API keys"
        ],
        "summary": "Create an API key",
        "description": "Creates a delivery API key for the fragment endpoint. **The full key is in this response only**: store it right away; afterwards only its prefix is listed.\n\nNot repeatable: every call creates a new key. Never retry blindly after a lost response — list the keys, revoke the one you never received, then create another.",
        "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/KeyWrite"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The key was created.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "`body` (not a JSON object) or `name` (not a string).",
            "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": "name must be a string",
                        "field": "name"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS -X POST \"https://api.shftd2.com/v1/keys\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"CDN\"}'"
          }
        ]
      }
    },
    "/v1/keys/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/KeyId"
        }
      ],
      "delete": {
        "operationId": "revokeKey",
        "tags": [
          "API keys"
        ],
        "summary": "Revoke an API key",
        "description": "Revokes a key: the fragment endpoint refuses it from the next request. Repeatable: revoking it again answers `200` with the first revocation time. The key is not deleted — deleting a revoked key is a console action. The body is `{}`.",
        "requestBody": {
          "required": true,
          "description": "Send `Content-Type: application/json` and `{}`.",
          "x-max-body-bytes": 262144,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmptyBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key, revoked.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Key"
                },
                "examples": {
                  "revoked": {
                    "value": {
                      "id": "key_examplekey01",
                      "name": "CDN",
                      "prefix": "ds_live_1a2b",
                      "created_at": "2026-09-26T09:00:00.000Z",
                      "last_used_at": null,
                      "revoked_at": "2026-09-26T09:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No key of this organisation 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": "`body`: not a JSON object.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "body": {
                    "value": {
                      "error": {
                        "code": "validation",
                        "message": "the request body must be a JSON object",
                        "field": "body"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "curl",
            "source": "curl -sS -X DELETE \"https://api.shftd2.com/v1/keys/key_examplekey01\" \\\n  -H \"Authorization: Bearer $DS_MANAGEMENT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'"
          }
        ]
      }
    }
  },
  "components": {
    "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."
      }
    },
    "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"
      },
      "RuleId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The rule id, from `GET /v1/sites/{domain}/routes`.",
        "schema": {
          "type": "string"
        },
        "example": "00000000-0000-4000-8000-000000000001"
      },
      "CardId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The card id (`crd_…`), from `GET /v1/cards`.",
        "schema": {
          "type": "string"
        },
        "example": "crd_examplecard1"
      },
      "KeyId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The key id (`key_…`), from `GET /v1/keys`.",
        "schema": {
          "type": "string"
        },
        "example": "key_examplekey01"
      }
    },
    "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"
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Me": {
        "type": "object",
        "description": "The organisation a management token acts on, and the token itself.",
        "required": [
          "account",
          "token"
        ],
        "properties": {
          "account": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "The organisation id (`acc_…`), also its `partner_id` on the fragment endpoint."
              },
              "name": {
                "type": "string",
                "description": "The organisation name."
              }
            }
          },
          "token": {
            "type": "object",
            "required": [
              "id",
              "prefix"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "The management token id (`mgt_…`), as listed in **Settings → Management tokens**."
              },
              "prefix": {
                "type": "string",
                "description": "The token prefix shown in the console (`ds_mgmt_` and four characters). Never the full token."
              }
            }
          }
        },
        "example": {
          "account": {
            "id": "acc_example1",
            "name": "Example Media"
          },
          "token": {
            "id": "mgt_exampletok01",
            "prefix": "ds_mgmt_9f8e"
          }
        }
      },
      "Site": {
        "type": "object",
        "description": "A site: a domain the organisation publishes on.",
        "required": [
          "id",
          "account_id",
          "domain",
          "name",
          "favicon_url",
          "default_fragment_id",
          "default_fragment_ref",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable site id (`sit_…`)."
          },
          "account_id": {
            "type": "string",
            "description": "The organisation id (`acc_…`)."
          },
          "domain": {
            "type": "string",
            "description": "The bare domain, normalized: lowercase, without scheme, path or port. `www.` and the apex are two sites."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name, or null."
          },
          "favicon_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The site icon. Null for a site created through this API until the site is first opened in the console."
          },
          "default_fragment_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The site default fragment id (`frg_…`), or null when the site has no default."
          },
          "default_fragment_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "The site default fragment reference; null when the site has no default, or when that fragment has no reference."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation time, UTC."
          }
        },
        "example": {
          "id": "sit_examplesite1",
          "account_id": "acc_example1",
          "domain": "news-site.example",
          "name": "News site",
          "favicon_url": null,
          "default_fragment_id": "frg_examplefr1",
          "default_fragment_ref": "/1234567/news-site/organic/politics",
          "created_at": "2026-09-26T09:00:00.000Z"
        }
      },
      "Fragment": {
        "type": "object",
        "description": "A fragment: one card slot on a site, addressed by its reference.",
        "required": [
          "id",
          "site_id",
          "domain",
          "ref",
          "name",
          "description",
          "lang",
          "key_values",
          "status",
          "created_at"
        ],
        "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."
          }
        },
        "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"
        }
      },
      "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"
          ]
        }
      },
      "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"
        }
      },
      "RuleDeleted": {
        "type": "object",
        "required": [
          "id",
          "deleted"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The deleted rule id."
          },
          "deleted": {
            "type": "boolean",
            "const": true
          }
        },
        "example": {
          "id": "00000000-0000-4000-8000-000000000001",
          "deleted": true
        }
      },
      "Card": {
        "type": "object",
        "description": "A card, as listed by this API. The card content itself is never returned here.",
        "required": [
          "id",
          "brand_surface",
          "advertiser_id",
          "status",
          "account_id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable card id (`crd_…`)."
          },
          "brand_surface": {
            "type": "string",
            "description": "The brand name the card presents."
          },
          "advertiser_id": {
            "type": "string",
            "description": "The advertiser join key."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "archived"
            ]
          },
          "account_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The owning organisation, or null for a platform card managed by doubleshift: readable, never writable with a management token."
          }
        },
        "example": {
          "id": "crd_examplecard1",
          "brand_surface": "Example Brand",
          "advertiser_id": "example-brand",
          "status": "active",
          "account_id": "acc_example1"
        }
      },
      "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"
        }
      },
      "Key": {
        "type": "object",
        "description": "A delivery API key, as listed. Never the full key, never its hash.",
        "required": [
          "id",
          "name",
          "prefix",
          "created_at",
          "last_used_at",
          "revoked_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable key id (`key_…`)."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key name, or null."
          },
          "prefix": {
            "type": "string",
            "description": "The first characters of the key, shown to recognise it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation time, UTC."
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last use on the fragment endpoint, or null."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "First revocation time, or null for an active key."
          }
        },
        "example": {
          "id": "key_examplekey01",
          "name": "CDN",
          "prefix": "ds_live_1a2b",
          "created_at": "2026-09-26T09:00:00.000Z",
          "last_used_at": null,
          "revoked_at": null
        }
      },
      "KeyCreated": {
        "type": "object",
        "description": "A delivery API key just created. This is the only response that ever holds the full key.",
        "required": [
          "id",
          "name",
          "prefix",
          "key",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable key id (`key_…`)."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key name, or null."
          },
          "prefix": {
            "type": "string",
            "description": "The first 12 characters of the key: what the console and `GET /v1/keys` show from now on."
          },
          "key": {
            "type": "string",
            "description": "The full key: `ds_live_` followed by 40 lowercase hexadecimal characters. Shown in this response only — store it now. It cannot be read again."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation time, UTC."
          }
        },
        "example": {
          "id": "key_examplekey01",
          "name": "CDN",
          "prefix": "ds_live_1a2b",
          "key": "ds_live_1a2b…",
          "created_at": "2026-09-26T09:00:00.000Z"
        }
      },
      "SiteList": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Site"
            }
          }
        },
        "example": {
          "items": [
            {
              "id": "sit_examplesite1",
              "account_id": "acc_example1",
              "domain": "news-site.example",
              "name": "News site",
              "favicon_url": null,
              "default_fragment_id": "frg_examplefr1",
              "default_fragment_ref": "/1234567/news-site/organic/politics",
              "created_at": "2026-09-26T09:00:00.000Z"
            }
          ]
        }
      },
      "FragmentList": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Fragment"
            }
          }
        },
        "example": {
          "items": [
            {
              "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"
            },
            {
              "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"
            }
          ]
        }
      },
      "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"
            }
          ]
        }
      },
      "CardList": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Card"
            }
          }
        },
        "example": {
          "items": [
            {
              "id": "crd_examplecard1",
              "brand_surface": "Example Brand",
              "advertiser_id": "example-brand",
              "status": "active",
              "account_id": "acc_example1"
            },
            {
              "id": "crd_platformcrd1",
              "brand_surface": "Example Platform Brand",
              "advertiser_id": "example-platform-brand",
              "status": "active",
              "account_id": null
            }
          ]
        }
      },
      "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"
            }
          ]
        }
      },
      "KeyList": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Key"
            }
          }
        },
        "example": {
          "items": [
            {
              "id": "key_examplekey01",
              "name": "CDN",
              "prefix": "ds_live_1a2b",
              "created_at": "2026-09-26T09:00:00.000Z",
              "last_used_at": null,
              "revoked_at": null
            }
          ]
        }
      },
      "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"
          }
        }
      },
      "SiteWrite": {
        "type": "object",
        "additionalProperties": false,
        "description": "Both fields are optional; `{}` creates the site without a name, or changes nothing on an existing one. `domain` is not a field: the domain is the address.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Absent: the current name is kept (none on creation). Trimmed; `\"\"` or a blank name clears it."
          },
          "default_fragment_ref": {
            "type": "string",
            "pattern": "^[A-Za-z0-9/_.:-]{1,200}$",
            "description": "Absent: the current default is kept. Present: the reference of an **active** fragment of this site, exact and case-sensitive (not trimmed), which becomes the default — so it is refused on the call that creates the site."
          }
        },
        "example": {
          "name": "News site"
        }
      },
      "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"
            ]
          }
        }
      },
      "DefaultFragmentWrite": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ref"
        ],
        "properties": {
          "ref": {
            "type": "string",
            "pattern": "^[A-Za-z0-9/_.:-]{1,200}$",
            "description": "The reference of an **active** fragment of this site, as a plain JSON string (not URL-encoded), exact and case-sensitive: it is not trimmed."
          }
        },
        "example": {
          "ref": "/1234567/news-site/organic/politics"
        }
      },
      "EmptyBody": {
        "type": "object",
        "additionalProperties": false,
        "maxProperties": 0,
        "description": "An empty JSON object, `{}`. A DELETE without it is refused (`400`).",
        "example": {}
      },
      "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"
        }
      },
      "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"
            }
          ]
        }
      },
      "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"
        }
      },
      "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"
            }
          ]
        }
      },
      "KeyWrite": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Optional. Trimmed; absent, empty or blank: no name. Names need not be unique."
          }
        },
        "example": {
          "name": "CDN"
        }
      }
    }
  }
}
