openapi-management.json.
Authentication and tokens
Every call sends a management token in theAuthorization 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.comonly:app.shftd2.comanswers404on these paths. Cookies are never read, so being signed in to the console changes nothing. A call withoutAuthorization: Beareris a401withWWW-Authenticate: Bearer realm="doubleshift".
account.id is also your partner_id on the fragment endpoint.
Limits and errors
EveryPUT, 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 forRetry-Afterbefore 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.exampleandnews-site.exampleare two different sites. -
A reference is case-sensitive:
/Politicsand/politicsare two fragments. It is 1 to 200 characters amongA-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 with422, 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
PUTon a site or a fragment creates or updates the object at that address:201the first time,200afterwards. Sending the same body again leaves the same state, so a script can run twice.PUT …/routesandPUT …/eligibilityreplace 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 …/routesgives every rule a newid. Do not keep rule ids across a replacement.- Deleting a rule is not repeatable: a second
DELETEof the same id is a404. Clearing the site default and revoking a key are repeatable: the second call answers200with the same state. POST …/routesadds one rule and answers409if it already exists.POST /v1/keysis 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.
nameabsent keeps the current name;""clears it.default_fragment_refabsent 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).domainin 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.
PUT /v1/sites/{domain}/fragments/{ref} creates or updates the fragment with that reference.
The body describes the whole fragment:
nameis required.langisfr,en,de,esorit. Absent, it isfr— on an update too.descriptionabsent or empty: none.key_valuesabsent:{}. Otherwise an object of non-empty lists of non-empty strings,{"section": ["politics"]}: keys ina-z 0-9 _ -(the key__proto__is refused), values trimmed and without commas or line breaks. Key-values are legacy targeting (see targeting).statusisactiveorpaused. Absent:activeon creation, unchanged on update.
204. The
response says so in warnings, for example ["site default", "target of 2 rules"]; otherwise warnings is
[].
Defaults and routing rules
These decide which fragment a page resolves to onGET /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.
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 …/routeswith{"routes": [{"path_prefix", "match", "ref"}]}replaces them all.POST …/routeswith{"path_prefix", "match", "ref"}adds one:409if the same path and match mode already exist,422once the site has 200 rules.DELETE …/routes/{id}with{}removes one.
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/cardslists your organisation’s cards and the platform cards ("account_id": null), in every status (draft,active,archived), by id.GET /v1/cards/{id}/eligibilitylists 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}/eligibilitywith{"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 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/keyswith an optional{"name"}creates one, and answers201with the full key inkey: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-characterprefixis listed.- A
POSTis 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/keyslists 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 answers200with the first revocation date. A revoked key is deleted in the console.
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:- Reads the token from its environment — never from an argument or a file — and never prints it.
- Calls
GET /v1/mefirst, and stops unlessaccount.idis the organisation it means to write to. - Reads before it writes:
GETthe object, compare it with the wanted state, and write only what differs. Compare paths and domains in their stored form (lowercase), references exactly. - 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. - Spaces its writes, about one per second, and on a
429waits forRetry-After. - 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.
- Runs twice in a row with the second run changing nothing.
- 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.
