Skip to main content
The management API lets a script write what you otherwise set up in the console: sites, fragments, the site default, routing rules, the fragments each card is eligible on (its eligibility), and API keys. It applies the same rules and returns the same error messages as the console forms. Use it to seed many sites at once — one fragment per ad-unit family, one rule per section — and to keep them in sync. It is separate from the fragment endpoint that serves cards: it takes another credential, and it never serves a card. Every endpoint has its own page under Endpoints, generated from the OpenAPI description openapi-management.json.

Authentication and tokens

Every call sends a management token in the Authorization header:
  • An owner of the organisation creates the token in the console, in Settings → Management tokens. The token is shown once, when it is created: copy it then. The console then lists its name, its prefix, who created it, its expiry and when it was last used.
  • A token acts on the whole organisation: every site, fragment, card and API key. There are no finer rights.
  • Revoke it there as soon as the script is done, or at once if it may have leaked: the next call answers 401. A token created by an owner does not expire; one created by doubleshift support expires after 90 days.
  • A management token (ds_mgmt_…) is not an API key (ds_live_…). The fragment endpoint refuses a management token, and the management API refuses an API key.
  • The API exists on api.shftd2.com only: app.shftd2.com answers 404 on these paths. Cookies are never read, so being signed in to the console changes nothing. A call without Authorization: Bearer is a 401 with WWW-Authenticate: Bearer realm="doubleshift".
Keep the token out of your shell history and out of files. In a terminal:
Then check which organisation the token writes to. This is always the first call of a script:
account.id is also your partner_id on the fragment endpoint.

Limits and errors

Every PUT, POST and DELETE sends Content-Type: application/json and a JSON object — {} for a DELETE. The body is at most 262 144 bytes, counted before it is parsed. A field the endpoint does not know is refused, nested ones included: send only the documented fields. Every response is JSON with Cache-Control: private, no-store. Lists are {"items": [...]} and complete: there is no pagination. Errors are {"error": {"code", "message", "field"?}}. Branch on code and field; message is the console’s wording, meant for people. The checks run in this order, and nothing is written until they all pass: the token and the limits, then the body (400, then 422 on its shape), then the site, card or key the path names (404), then what depends on what is stored (403, 409, 422). So an invalid fragment body sent to an unknown site is a 422, not a 404. Limits:
  • Per token: 120 requests per minute, of which at most 60 writes (PUT, POST, DELETE).
  • Per IP address: 30 failed authentications per minute, and 600 requests per minute that present a well-formed token.
  • Every response carries RateLimit-Policy: "token";q=120;w=60, "write";q=60;w=60. No header tells you how many calls remain.
  • The counters are kept per Cloudflare location, so the limits are approximate. Space your writes — one per second stays well within them — and on a 429, wait for Retry-After before you retry.

References

A fragment is addressed by its site’s domain and its reference: the GAM ad-unit path of the inventory without its format, e.g. /1234567/news-site/organic/politics (see fragments).
  • {domain} is the site’s bare domain: lowercase, without scheme, path or port. www.news-site.example and news-site.example are two different sites.
  • A reference is case-sensitive: /Politics and /politics are two fragments. It is 1 to 200 characters among A-Z a-z 0-9 / _ - . :, and no path segment may be . or .. on its own: URL processing removes such segments, so the reference could not be addressed. A dot inside a segment is fine: .name, a.b, /v1.2/politics.
  • In a path, encode the whole reference once with encodeURIComponent, slashes included, so it travels as one segment:
    Encode it once, never twice: a double-encoded reference (%252F…) is refused with 422, and unencoded slashes match no endpoint (404).
  • In a JSON body — ref, default_fragment_ref, routes[].ref, fragments[].ref — send the reference as a plain string, never encoded.
  • A fragment created in the console without a reference is listed with "ref": null, but the API cannot write it, make it the default, target it with a rule or make a card eligible on it. Give it a reference in the console first.
  • A reference cannot be changed through the API: it is the fragment’s address.

Writes and idempotency

  • PUT on a site or a fragment creates or updates the object at that address: 201 the first time, 200 afterwards. Sending the same body again leaves the same state, so a script can run twice.
  • PUT …/routes and PUT …/eligibility replace the whole list. Each replacement is checked entirely, then applied at once: on any error nothing changes. But there is no transaction across calls, and nothing protects a replacement from another client writing the same list at the same time: the last write wins. Read the list just before you replace it, and never run two scripts on the same organisation at once.
  • An empty list is destructive: {"routes": []} deletes every rule of the site, and {"fragments": []} makes your card, when active, eligible on every fragment of your organisation.
  • PUT …/routes gives every rule a new id. Do not keep rule ids across a replacement.
  • Deleting a rule is not repeatable: a second DELETE of the same id is a 404. Clearing the site default and revoking a key are repeatable: the second call answers 200 with the same state.
  • POST …/routes adds one rule and answers 409 if it already exists.
  • POST /v1/keys is not repeatable: each call creates a new key (see Delivery keys).

Sites and fragments

Sites. PUT /v1/sites/{domain} registers the site in your organisation, or updates it.
  • name absent keeps the current name; "" clears it.
  • default_fragment_ref absent keeps the current default. When present, it must be an active fragment of this site — so it cannot be set in the call that creates the site (see Defaults and routing rules).
  • domain in the body is refused: the domain is the address, and it never changes. To move to another domain, register it as a new site.
  • The site is created in your organisation even if another organisation has registered the same domain.
  • A site created through the API has no icon until you first open it in the console.
Fragments. PUT /v1/sites/{domain}/fragments/{ref} creates or updates the fragment with that reference. The body describes the whole fragment:
  • name is required.
  • lang is fr, en, de, es or it. Absent, it is fr — on an update too.
  • description absent or empty: none.
  • key_values absent: {}. Otherwise an object of non-empty lists of non-empty strings, {"section": ["politics"]}: keys in a-z 0-9 _ - (the key __proto__ is refused), values trimmed and without commas or line breaks. Key-values are legacy targeting (see targeting).
  • status is active or paused. Absent: active on creation, unchanged on update.
On an update, a field you leave out is reset, not kept: lang goes back to fr, the description is removed and key_values becomes {}. Only status is kept. Send every field every time.
Pausing a fragment keeps the site default and the rules that point to it: their pages answer 204. The response says so in warnings, for example ["site default", "target of 2 rules"]; otherwise warnings is [].
Sites are listed by domain; fragments by reference, those without one last.

Defaults and routing rules

These decide which fragment a page resolves to on GET /v1/fragment: an exact rule, then the longest prefix rule, then the site default (see sites). Default. PUT /v1/sites/{domain}/default-fragment with {"ref": …} sets it: an active fragment of this site. DELETE with {} removes it; the fragment itself is kept. A default that is paused afterwards stays the default, and its pages answer 204; but a paused fragment cannot be newly set as default. To reproduce that state, write the fragment active, set the default, then pause the fragment.
Routing rules. They follow the console’s rules: match is exact or prefix; the path is stored lowercase with its canonical encoding (Actualite/ becomes /actualite/), 512 characters at most as sent (trimmed) and 200 once canonical; ref names a fragment of this site, a paused one included. A site has at most 200 rules, listed by path then match mode.
  • PUT …/routes with {"routes": [{"path_prefix", "match", "ref"}]} replaces them all.
  • POST …/routes with {"path_prefix", "match", "ref"} adds one: 409 if the same path and match mode already exist, 422 once the site has 200 rules.
  • DELETE …/routes/{id} with {} removes one.
PUT …/routes replaces every rule of the site. Rules missing from the list are deleted, and {"routes": []} deletes them all. Read the current rules first. The list is checked entirely — ceiling, paths, references, duplicates after canonicalization — then applied at once: on any error nothing changes.
To delete one rule, set RULE_ID to an id from GET …/routes:

Cards and eligibility

Cards are created, edited and activated in the console. The API lists them and sets the fragments each card is eligible on — its eligibility (see eligibility). Several cards can be eligible on the same fragment; doubleshift then picks one per page.
  • GET /v1/cards lists your organisation’s cards and the platform cards ("account_id": null), in every status (draft, active, archived), by id.
  • GET /v1/cards/{id}/eligibility lists the fragments of your organisation the card is eligible on. For one of your cards, an empty list ([]) means it is eligible everywhere in the organisation.
  • PUT /v1/cards/{id}/eligibility with {"fragments": [{"domain", "ref"}]} sets that list. Each fragment is named by its site’s domain and its reference, as a plain string. Only your organisation’s fragments are accepted — a paused one too — and a card in any status can be made eligible. The same fragment named twice counts once.
PUT …/eligibility replaces the card’s eligibility in your organisation. Fragments missing from the list lose the card, and {"fragments": []} empties the list: your card, when active, is then eligible everywhere in the organisation. Read the current list first. The list is checked entirely, then applied at once.
Platform cards are managed by doubleshift. You can read them and their eligibility on your fragments, but PUT …/eligibility answers 403 platform_card. For a platform card an empty list is not conclusive: the eligibility other organisations set is never shown, so the card may be eligible everywhere, or only on their fragments. Set CARD_ID to an id from GET /v1/cards:

Delivery keys

Delivery keys are the API keys your edge sends to the fragment endpoint.
  • POST /v1/keys with an optional {"name"} creates one, and answers 201 with the full key in key: ds_live_ followed by 40 hexadecimal characters. The key is in that response only: store it at once in your secret store. Afterwards only its 12-character prefix is listed.
  • A POST is not repeatable: each call creates a new key. If a response is lost, do not retry blindly: list the keys, revoke the one you never received, then create another.
  • GET /v1/keys lists every key, revoked ones included, newest first. It never shows the full key.
  • DELETE /v1/keys/{id} with {} revokes a key: the fragment endpoint refuses it from the next call. Revoking it again answers 200 with the first revocation date. A revoked key is deleted in the console.
The POST response holds a secret: keep it out of logs and tickets. Set KEY_ID to an id from GET /v1/keys to revoke that key:

Prepare a client

A script that seeds or syncs an organisation:
  1. Reads the token from its environment — never from an argument or a file — and never prints it.
  2. Calls GET /v1/me first, and stops unless account.id is the organisation it means to write to.
  3. Reads before it writes: GET the object, compare it with the wanted state, and write only what differs. Compare paths and domains in their stored form (lowercase), references exactly.
  4. Writes in this order, site by site: the site, its fragments, the default, the rules. Card eligibility comes last, once every site is done, because it names fragments the earlier steps create — and not at all if an earlier step failed. When the wanted state lives in a manifest, keep eligibility in its own section — for example "eligibility": [{"advertiser_id", "fragments": [{"domain", "ref"}]}] — with one complete list per card.
  5. Spaces its writes, about one per second, and on a 429 waits for Retry-After.
  6. Leaves keys and cards aside. It creates an API key only when one is needed, in a separate step, and keeps the secret the response returns. Cards must already exist: they are created in the console.
  7. Runs twice in a row with the second run changing nothing.
  8. Ends with the token revoked in Settings → Management tokens.
This documentation has an MCP server, https://docs.shftd2.com/mcp, that lets an AI assistant search these pages. It does not call the management API.

Console-only actions

The API does not do these; the console does.
  • Creating, editing, activating, archiving or deleting a card: the console’s editorial flow.
  • Deleting a site or a fragment, which removes what depends on it. The console asks for confirmation. Through the API, pause a fragment instead.
  • Deleting a revoked API key.
  • Creating, revoking and deleting management tokens (owners only).
  • Inviting and managing the organisation’s members.