diff --git a/images/51did-two-step-verification.svg b/images/51did-two-step-verification.svg
new file mode 100644
index 0000000000..2487e95426
--- /dev/null
+++ b/images/51did-two-step-verification.svg
@@ -0,0 +1,40 @@
+
diff --git a/src/devicedetection/otherintegrations/wordpress.md b/src/devicedetection/otherintegrations/wordpress.md
index e147021561..cbeaa619bf 100644
--- a/src/devicedetection/otherintegrations/wordpress.md
+++ b/src/devicedetection/otherintegrations/wordpress.md
@@ -59,7 +59,7 @@ Advanced mode relies on a probabilistic identifier from your Resource Key - `IdP
## Preference Management Platform (PMP)
-The `PMP` tab adds a 51Degrees consent popup to your public pages. Visitors choose `Standard`, `Personalized`, or a publisher-defined alternative (for example "Remove ads" or "Subscribe"). The choice is stored client-side in `localStorage` under the `__51d_pmp_pref` key - no cookies and no extra server round-trips - and PMP acts as the CMP by exposing the consent state through the IAB TCF API, so a separate consent manager is not needed. For the underlying concept and the cloud endpoint, see @ref Identifiers_PMP.
+The `PMP` tab adds a 51Degrees consent popup to your public pages. Visitors choose `Standard`, `Personalized`, or a publisher-defined alternative (for example "Remove ads" or "Subscribe"). The choice is kept in a first party cookie on your own domain, `__mtm_pref`, holding one of `standard`, `personalized` or `non-marketing`. The answer is a cookie rather than browser storage because a request carries cookies, and carries nothing else PMP could write, so your own server, and anything in front of it, can act on the answer. The cookie carries no `HttpOnly` flag, which would stop a script reading it, because the page reads it back and replaces it when the visitor changes their mind. PMP also acts as the consent management platform (CMP) by exposing the consent state through the IAB Transparency and Consent Framework (TCF) API, so a separate consent manager is not needed. For the underlying concept and the cloud endpoint, see @ref Identifiers_PMP.
Configure the popup from the `PMP` tab:
@@ -75,7 +75,7 @@ The popup works out of the box. To react to the visitor's choice, override the g
```json
```
diff --git a/src/identifiers/fodid.md b/src/identifiers/fodid.md
index 70202c3cc5..a64c79e045 100644
--- a/src/identifiers/fodid.md
+++ b/src/identifiers/fodid.md
@@ -7,9 +7,9 @@ A signed envelope, encoded as an |
+| Platform | Package | Distribution |
+|----------|-----------------------------------|--------------------------------------------------------------------|
+| .NET | `FiftyOne.Did` | |
+| Java | `com.51degrees:pipeline.did` | |
+| Node.js | `fiftyone.pipeline.did` | |
+| Python | `fiftyone-pipeline-did` | |
+| PHP | `51degrees/fiftyone.pipeline.did` | |
-Readers for other platforms are on the roadmap and will be added to this table as they are released.
+Every reader exposes the same surface, set out in the [package surface](https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md) part of the specification, which also defines it for Rust. A reader parses the payload and exposes the usage and whether it was stated directly, the type, the licence id, the match key and the address of the terms document the identifier was issued under, and it refuses a payload whose version it does not know, naming the version. Reading an identifier and verifying its signature are separate questions, and the signature is verified through the envelope's own reader against a public key. The byte offsets stay out of every package's public surface so that application code never reads the layout by hand. The .NET package is the reference implementation.
## Comparing two 51Dids
-Two 51Dids issued for the same device + IP + usage will differ at the byte level because the envelope embeds a fresh timestamp and signature on each call. The byte-level difference is in the **identifier** (the wrapper); the **probabilistic value** carried inside is stable across reissues. To decide whether the two refer to the same browser instance, compare the probabilistic values, never the full base64 identifiers.
+Two 51Dids issued for the same device + IP + usage will differ at the byte level because the envelope embeds a fresh timestamp and signature on each call. The byte-level difference is in the **identifier** (the wrapper), whereas the **match key** carried inside is stable across reissues. To decide whether the two refer to the same browser instance, compare the match keys, never the full base64 identifiers.
-The probabilistic value is one of the fields the reader exposes after parsing the payload (per platform, named `Hash` in .NET to reflect that it is a 32-byte SHA-256). Treat it as the cache / dedup key.
+The match key is one of the fields the reader exposes after parsing the payload, as `MatchKey` in every language. Treat it as the key for caching and for spotting duplicates.
Two responses to the same device + IP + `id.usage=non-marketing`, returned a few seconds apart:
@@ -191,27 +206,106 @@ var b = new FodId(idprobglobalB);
Console.WriteLine(a.Date == b.Date); // false
Console.WriteLine(a.Signature.SequenceEqual(b.Signature)); // false
-// The probabilistic value inside the payload IS stable; this is
-// what you actually compare:
-Console.WriteLine(a.Hash.SequenceEqual(b.Hash)); // true
+// The match key inside the payload IS stable; this is what you
+// actually compare:
+Console.WriteLine(a.MatchKey.SequenceEqual(b.MatchKey)); // true
```
-Use `FodId.Hash` (32 bytes, SHA-256, the probabilistic value) as the cache / dedup key. The same value means the same browser instance under the same usage policy on the same License Key (for `idproblic`) or across all callers (for `idprobglobal`).
+Use `FodId.MatchKey` (32 bytes for the probabilistic and hashed email types) as the key for caching and for spotting duplicates. The same value means the same browser instance under the same usage policy on the same License Key (for `idproblic`) or across all callers (for `idprobglobal`).
## Validation
-A 51Did recipient can optionally verify the signature before trusting the identifier. Two options:
+Two things about a 51Did can be checked. The signature says whether the identifier is an authentic 51Degrees 51Did that has not been altered, and the creator context says whether it is being presented from the browser and connection it was created on. They are independent, and the second is described after the first.
+
+A 51Did recipient can verify the signature before trusting the identifier. Two options:
1. **Cloud endpoint.** Send the base64 value to the verification endpoint on the V4 cloud and get back a parsed payload only if the signature checks out. Simple, no key handling, but every call is metered against the Resource Key.
2. **Local verification using the published public key.** Fetch 51Degrees' public ECDSA P-256 key once, cache it, and verify in-process for every received identifier. No metering. Each platform reader (see *51Did readers* above) exposes an in-process verify method that takes the public key PEM and returns a boolean. The .NET reader's method is the inherited `Owid.VerifyAsync`.
-In both cases, validation only confirms the identifier was created by 51Degrees and has not been tampered with. It does not certify that the device + IP + usage inputs were truthful: that trust lives in the operational contract with the issuing 51Degrees cloud, not in the signature.
+In both cases, signature validation only confirms the identifier was created by 51Degrees and has not been tampered with. It does not certify that the device + IP + usage inputs were truthful, because that trust lives in the operational contract with the issuing 51Degrees cloud, not in the signature.
+
+### Verifying the creator context
+
+The creator context is what the 51Degrees cloud, as the creator of the identifier, recorded about the creating request when it issued the identifier. Verifying it confirms the 51Did is being presented from the browser and connection it was created on. It is checked only within the 51Degrees service, which alone holds the key the context is made under, and every check is metered against the Resource Key. Identifiers issued by the 51Degrees cloud carry a creator context from the release that introduced it, and one created before that, or by a self-hosted deployment with the creator context switched off, reports `nocontext`.
+
+The check is made in two steps, so that the verdict never exists in the browser in a form the browser can read, alter or forge.
+
+
+
+- **Step one.** The page calls `verify-context` (or `verify-full`) from the visitor's browser with the 51Did and the page's Resource Key (see @ref Services_Cloud_ResourceKeys), and receives `{ "result": "..." }`, an opaque sealed value and nothing else. The call must come from the browser presenting the identifier, because the service compares the identifier against the connection making the call, so use a `fetch` that reads the JSON response rather than a script tag or a pixel. The page may also pass a `challenge`, a single-use value your server issued for this transaction, which is folded into the result.
+- The page passes the 51Did and the sealed result to your server as part of its normal request, for example with the form post or the purchase the identifier is being trusted for.
+- **Step two.** Your server calls `redeem` with the 51Did it holds, the sealed result, a licence key of the account whose Resource Key made the verification (required wherever the account holds licence keys, and never placed in a page) and, where a `challenge` was given at step one, the same value again, and receives the verdict.
+
+The endpoints on the V4 cloud, all of which take the identifier as the `51did` parameter on the query string, in a form, or as the route `51did/`:
+
+- `GET`/`POST` `/api/v4/id/verify` returns `{ "valid": }`, the signature result only, readable at once.
+- `GET`/`POST` `/api/v4/id/verify-context` returns `{ "result": "..." }`, a sealed context result.
+- `GET`/`POST` `/api/v4/id/verify-full` returns `{ "result": "..." }`, a sealed result carrying the signature result as well as the context result, so one call and one redemption give both.
+- `GET`/`POST` `/api/v4/id/redeem` takes `51did`, `result`, `license` and optionally `challenge`, and returns `{ "signature": "verified" | "invalid", "context": "...", "factors": { ... }, "verifiedAt": "...", "secondsSinceVerified": }`.
+
+All four require a Resource Key and are metered against it. A call with no Resource Key, or whose Resource Key lacks the entitlement, returns `401`. A `license` parameter may add entitlement but is not an alternative to the Resource Key. Every call is one use, so checking the creator context from a browser costs two uses, one for the verification and one for the redemption, whereas checking the signature alone with `verify` costs one. The self-hosted container does not count uses per call.
+
+**Send what the page collected.** The context is compared against the device as the cloud has fully resolved it, exactly as the identifier was created against it (see *When the identifier is created*). After the client script reports complete, include every value it stored, such as `51D_ProfileIds`, `51D_ScreenPixelsWidth` and, on a Chromium browser, `51D_GetHighEntropyValues`, as parameters on the verification request. A page's verification that carries none of them, where the device then differs and the same browser made the request, is answered with a `400` naming the values to send rather than a verdict. If collection cannot complete, for example because the client script failed to load, do not verify and do not report a mismatch. Report that the check did not complete, which is not evidence either way.
+
+The `context` values:
+
+| Value | Meaning |
+|-------|---------|
+| `verified` | The 51Did is being presented from the browser and connection it was created on. |
+| `mismatch` | At least one factor of the presenting browser or connection differs from the one recorded at creation. `factors` says which. |
+| `misconfigured` | The service that checked the identifier could not complete the check, and the reason is that service rather than the identifier. Nothing a caller sends can produce it. Where some factors were compared, `factors` marks the rest `misconfigured` and shows the outcome of the ones it could check. Not a mismatch, and not to be treated as one. Against your own hosted service, its start-up log names the setting to change. |
+| `invaliddate` | The identifier claims a creation date the scheme could not have produced, being in the future or before the creator context existed, so the identifier is fabricated and nothing is wrong with the service. |
+| `nocontext` | The 51Did carries no creator context to check, being one created before the creator context existed or by a deployment with it switched off. Normal rather than an error, and it says nothing about whether the identifier is genuine, which the signature answers on its own. |
+
+Where `context` is `mismatch`, or `misconfigured` with some factors compared, `factors` breaks the comparison down across nine independent factors named `transport`, `device`, `browserip`, `connectionip`, `asn`, `platformname`, `platformversion`, `browsername` and `browserversion`, each `verified`, `mismatch` or `misconfigured`. It is there to help you locate a problem and to weigh a mismatch. A call made from a server rather than the presenting browser, for example, shows the transport, device and connection factors as `mismatch`, the server having its own connection and device. Nothing about what a factor is made of is exposed, only whether it matched. Treat the top-level `context` value as the result.
+
+Read together with the signature:
+
+| `signature` | `context` | Meaning |
+|-------------|-----------|---------|
+| `verified` | `verified` | Authentic identifier presented from its creation context. |
+| `verified` | `mismatch` | Authentic identifier presented from a different context. What a replay looks like, and also what a legitimate server verifying out of context sees, and that server knows which situation it is in. |
+| `verified` | `nocontext` or `misconfigured` | Authentic identifier with no context this service could check. Rely on the signature alone. |
+| `invalid` | any | The envelope has been altered or corrupted. A creator context cannot be forged, because it is made under a key only 51Degrees holds, so a `verified` context on an `invalid` signature means the context data is intact and something else in the envelope is not. |
+
+**The redemption itself.** A sealed result redeems once, within ten seconds of the verification, and each verification produces a fresh one. The window is the anti-replay window of TLS 1.3, which QUIC mandates ([RFC 8446 section 8.3](https://www.rfc-editor.org/rfc/rfc8446#section-8.3)), so the trade-off between clock error, network variation and replay exposure has already been argued, and it is a constant of the service that cannot be widened by configuration. Every clock involved is a server clock, so the visitor's browser clock plays no part. In the genuine flow the two steps happen moments apart within one page transaction, so the window costs nothing. `verifiedAt` and `secondsSinceVerified` describe the verification you have just made and let your server apply a stricter rule of its own without any clock work. They say nothing about the identifier's age, which comes from the identifier itself.
+
+| Response | Meaning | What to do |
+| --- | --- | --- |
+| `context` of `expired`, with `verifiedAt` and `secondsSinceVerified` | Genuine but older than ten seconds | Treat as unverified, and if this recurs in a genuine flow redeem sooner after the page verifies |
+| `context` of `replayed` | Already redeemed on this service instance | Treat as unverified, because something presented the same result twice |
+| `context` of `unreadable` | Not a result sealed for this 51Did, licence key and challenge, or altered | Treat as unverified. The service does not say which was wrong |
+| `unconfirmed` (HTTP 503) | The instance could not confirm first use | Retry, as it is neither a replay nor a forgery |
+| HTTP 400 naming the values to send, such as `51D_ProfileIds` | A page verified before sending what its scripts collected | Send the values the client script collected and verify again |
+| HTTP 400 naming a payload status | The 51Did is not a shape the scheme produces | Treat as unverified, and check what produced the value you sent |
+
+The record of redemptions is held in memory per service instance rather than across regions, as TLS 1.3 notes of its own record in distributed deployments, so two redemptions of one result routed to two instances can both succeed within the window. If your flow must be single use everywhere, route redemptions for one transaction to one place or keep your own short-lived record of results already redeemed.
+
+**Identifiers issued with a creator context before release 4.4.37.** That release changed the layout of the creator context in place rather than adding a version, so a 51Did the cloud issued with a creator context before it is read as malformed and fails the signature check on every verify endpoint, `verify` included. Treat such an identifier as unverified and use a newly created 51Did in its place.
+
+**A mismatch is not always a problem.** The match key is what is stable, and nothing about the creator context changes it. What can change is the connection the visitor arrives on, so a mismatch says the visitor is arriving differently from before, not that the identifier is fake. A privacy relay service changes the address and the operator together. A home or mobile connection changes its address and keeps its operator, so `browserip` and `connectionip` mismatch with `asn` verified. A browser or operating system upgrade changes a version and keeps the name, so `browserversion` or `platformversion` mismatches with `browsername` and `platformname` verified. Read the factors with the identifier's age, which the identifier carries to the minute:
+
+| Age of the 51Did | What a mismatch suggests |
+| --- | --- |
+| Seconds to minutes | Treat seriously. The visitor has not moved network, no relay has rotated and no browser has updated in that time, so the likeliest explanation is that the identifier is being presented from somewhere other than where it was created. |
+| Hours to a day | Worth weight. A relay rotation or a new address is possible, a browser upgrade unlikely. |
+| Days to weeks | Weak on its own. Address changes and browser upgrades are both routine over this span. |
+| A month or more | Expect mismatches. A verified context after this long is a strong positive, whereas a mismatch is close to uninformative on its own. |
+
+Match your response to what you are about to do rather than to the verdict alone. For frequency capping, measurement and reporting, an address change or an upgrade is no reason to discard the identifier. For personalisation and audience selection, carry on and lower any confidence you keep. For signing in, changing an account or taking payment, ask for a second factor of your own on an address change or an upgrade, and refuse on a changed browser name, platform name or device, or on several unrelated factors at once. An identifier made minutes ago deserves the stricter response, and one made a month ago the more lenient. The service never withholds a verdict because of the identifier's age. It reports what matched and what did not, and the decision is yours.
+
+**What the identifier does not tell you about its creator.** Every 51Did carries a small field 51Degrees uses to know which of its own customers created the identifier, for support and billing. It is encrypted, it changes as 51Degrees rotates the secret behind it, and it also depends on the identifier itself, so two identifiers from the same customer carry different values. A recipient cannot group identifiers by the customer that created them, cannot tell whether two came from the same customer, and cannot work out who any customer is.
+
+Local public-key verification (option 2 above) covers the signature only. The creator context check exists nowhere but the 51Degrees service, and is available self-hosted through the bespoke Docker solution for identifiers that deployment creates. A self-hosted instance running without TLS capture still serves the whole flow for identifiers created and verified on that same instance, which is intended for local testing, and reports through its health check that it is not capturing, so the fault reaches whoever runs the deployment rather than the caller.
+
+A long-lived identifier that still verifies from its creation context is the strongest signal of a stable, real user, and age cannot be manufactured. This makes context verification well suited to a render-time check. Place the 51Did from a bid request into the creative, verify it from the rendering browser, send the 51Did and the sealed result to your own endpoint, and redeem them there. A `context` of `mismatch` on an identifier made minutes earlier means the paid impression rendered somewhere other than the browser the bid described.
+
### Fetching the public key for local verification
Local verification (option 2 above) fetches the key from the OWID creator endpoint, `GET /owid/api/v3/creator`. The response carries the current signing key in `publicKeySPKI` (PEM).
-The signing key rotates weekly, so a 51Did issued before the latest rotation was signed with an older key. To fetch the key that was current when a 51Did was created, pass its date: `GET /owid/api/v3/creator?date=`. The `date` is the same value the OWID envelope carries in its Date field, minutes since `2020-01-01T00:00:00Z` (see the [OWID explainer](https://github.com/SWAN-community/owid/blob/main/explainer.md), "Data Structure" section). The endpoint returns the signing key with the latest creation time on or before `date`; if `date` predates every known key it returns `404`, and a `date` that is not an unsigned 32-bit integer returns `400`.
+Signing keys belong to periods of a schedule, and a 51Did is signed with the key of the period its creation falls in, so a 51Did issued in an earlier period was signed with an earlier key. To fetch the key that was in force when a 51Did was created, pass its date on every request, as `GET /owid/api/v3/creator?date=`. The `date` is the same value the OWID envelope carries in its Date field, minutes since `2020-01-01T00:00:00Z` (see the [OWID explainer](https://github.com/SWAN-community/owid/blob/main/explainer.md), "Data Structure" section). The endpoint returns the signing key whose period was in force at `date`. If `date` predates every known key it returns `404`, and a `date` that is not an unsigned 32-bit integer returns `400`.
### Fetching every public key at once
@@ -229,9 +323,9 @@ The response is a JSON array, one entry per signing key:
]
```
-To fetch only the keys created since you last pulled, add an ISO 8601 UTC timestamp: `GET .../api/v4/id/key/datetime/2026-03-08T00:00:00Z?resource=`. The response then holds only keys created on or after that timestamp. This endpoint takes an ISO 8601 timestamp, not the minutes-since-2020 value that `/creator?date=` uses. Unlike `/creator`, it needs a Resource Key and is metered.
+To fetch only the keys created since you last pulled, add an ISO 8601 UTC timestamp, as `GET .../api/v4/id/key/datetime/2026-03-08T00:00:00Z?resource=`. The response then holds only keys created on or after that timestamp. This endpoint takes an ISO 8601 timestamp, not the minutes-since-2020 value that `/creator?date=` uses. Unlike `/creator`, it needs a Resource Key and is metered.
## Use cases
-- **Marketing** - PMP captures the user's preference and feeds it as `id.usage`; the 51Did is consumed by Prebid / RTB enrichment. See @ref Identifiers_PMP and @ref Integrations_Prebid.
+- **Marketing** - PMP captures the user's preference and feeds it as `id.usage`, and the 51Did is consumed by Prebid / RTB enrichment. See @ref Identifiers_PMP and @ref Integrations_Prebid.
- **Non-marketing** - the integrator sets `id.usage=non-marketing` server-side for fraud, bot or suspicious-activity detection (for example, the suspicious-activity module in the 51Degrees WordPress plugin). The identifier never leaves the customer environment.
diff --git a/src/identifiers/overview.md b/src/identifiers/overview.md
index a410939ded..88983da5bf 100644
--- a/src/identifiers/overview.md
+++ b/src/identifiers/overview.md
@@ -1,9 +1,9 @@
@page Identifiers_Overview Overview
-**51Did** (51Degrees Identifier) and **PMP** (Privacy Marketing Preference) are derived signals downstream systems can act on without seeing the raw inputs.
+**51Did** (51Degrees Identifier) and **PMP** (Preference Management Platform) are derived signals downstream systems can act on without seeing the raw inputs.
- **51Did** - signed identifier (a base64 OWID envelope) carrying a probabilistic value, derived from three inputs: the **Device ID** (a `Hardware-Platform-Browser-IsCrawler` tuple produced by Device Detection), the **client IP**, and the **usage purpose** (`non-marketing`, `standard`, or `personalized`) declared per request. See @ref Identifiers_51Did for the identifier-versus-value distinction.
-- **PMP** - embeddable widget that captures a marketing preference (`standard` / `personalized`) suitable as `id.usage` input to 51Did.
+- **PMP** - embeddable widget that captures a marketing preference (`non-marketing`, `standard` or `personalized`) suitable as `id.usage` input to 51Did.
## Flow
@@ -14,6 +14,6 @@ User → PMP → preference → id.usage → 51Did → downstream
## Use one without the other when
- **PMP alone** - you need to collect a user marketing preference and act on it directly (e.g. gate ad personalization, drive a paywall), without feeding it into 51Did.
-- **51Did alone** - `id.usage=non-marketing` (e.g. fraud or suspicious activity); the preference is set by the integrator, not the user.
+- **51Did alone** - `id.usage=non-marketing` (e.g. fraud or suspicious activity), where the preference is set by the integrator rather than the user.
See @ref Identifiers_51Did and @ref Identifiers_PMP. 51Did depends on @ref DeviceDetection_Overview.
diff --git a/src/identifiers/pmp.md b/src/identifiers/pmp.md
index bb1f9750dc..5af8115079 100644
--- a/src/identifiers/pmp.md
+++ b/src/identifiers/pmp.md
@@ -1,23 +1,34 @@
@page Identifiers_PMP PMP (Preference Management Platform)
-Lightweight embeddable widget that asks the user for a marketing preference, stores it in `localStorage`, and invokes a publisher-defined continuation URL with that preference - typically the 51Did generation endpoint. The chosen `standard` or `personalized` value is the `id.usage` input for @ref Identifiers_51Did.
+PMP is a small embeddable widget that asks a visitor what kind of marketing they want, keeps the answer in a cookie on your domain and announces it on the page. The answer is one of `non-marketing`, `standard` or `personalized`, which are the three `id.usage` values @ref Identifiers_51Did takes.
-## Endpoint
+## Endpoints
+
+A page view makes two requests. The tag on the page fetches the loader, and the loader works out which bundle the page needs and fetches that.
+
+```
+GET https://cloud.51degrees.com/api/v4/pmp/.js
+```
+
+That returns the loader, which is about a kilobyte and the same bytes for every caller. **The resource key is the file name in the URL and nothing else on this URL is read.** Every other setting is a `data-` attribute on the script tag, so each setting is written in one place and read from one place. Any query string a page adds here is ignored and is carried nowhere. This request is neither checked nor counted, so a visitor who leaves before the page settles costs nothing.
```
-GET https://cloud.51degrees.com/api/v4/pmp?resource=
+GET https://cloud.51degrees.com/api/v4/pmp//
```
-Returns the minified locale-resolved bundle. Locale is picked from the `accept-language` query parameter or the `Accept-Language` header, falling back to `en-us`.
+That returns one built bundle, where `` is a locale such as `en-us`, optionally followed by `-nosharing`. The loader picks the language from the visitor's own browser, matching the full tag first, then the language on its own, then falling back to `en-us`, so the language is decided in the browser rather than from a request header. Whether the bundle needs the sharing code comes from `data-use-third-party-cookies` and `data-network-name` on the tag. The loader adds `license=` where the tag carries `data-license-key`.
+
+The bundle is the request that is checked and counted, because it is what the visitor actually receives. **Your resource key must be registered for the domain the page is on.** The request is checked against the domains the key names, using the `Referer` header and falling back to `Origin`, the same as every other keyed endpoint, and a page on a domain the key does not cover is refused with 401 and no dialog appears. A page that suppresses the `Referer` header, with `` or an equivalent policy, is refused for the same reason, because the check has nothing to compare.
+
+**An older form of this endpoint took the resource key as a query parameter, `https://cloud.51degrees.com/api/v4/pmp?resource=`. That address answers 404 now, so a page still carrying it fetches nothing and shows no dialog.** The key also has to be in the URL rather than on the tag, because `data-resource-key` is not read. Two places to write a key meant a page could carry one the cloud never saw, and the refusal that followed was invisible to the page.
## Integration
-Add a single `
```
+### The 51Degrees client script belongs on the page as well
+
+PMP reads two things from the 51Degrees client script, being whether third party cookies work in this browser and whether the General Data Protection Regulation (GDPR) applies. The first decides whether the second card can be offered and the second is what PMP reports as `gdprApplies` through `__tcfapi`, the consent function that ad scripts call. Both come from properties on your resource key, `ThirdPartyCookiesEnabled` with `ThirdPartyCookiesEnabledJavaScript` and `IsGdpr`, so ask for those when you create the key and put the client script on the page too:
+
+```html
+
+```
+
+The two tags can go in either order and both can carry `async`, because the client script listens on the window for PMP's answer whilst PMP watches for the client script's object, which is `fod` unless `data-object-name` says otherwise. The object is the only thing PMP looks at, so it does not matter where the client script came from, and one served by a different 51Degrees cloud, by a proxy of your own or out of a bundle you built yourself all work the same way.
+
+An `async` tag has usually not run at the moment PMP starts, so an object that is not there yet is not an object that is never coming, and PMP waits for it instead of deciding straight away. The wait finishes once your page has loaded, since every tag written into the markup has had its turn by then, and it is capped at 5000 milliseconds for a page whose load event is very late. If nothing has appeared by the end of it, PMP loads the client script itself from the cloud that served PMP, using the resource key it already has, and says so in the console. Writing the tag yourself is still the better option, because then the placement and the timing are yours.
+
+Load the client script once. A second copy runs a second round and creates a second identifier, and the client script itself warns about it.
+
## Configuration attributes
-The full set of attributes the widget reads from its own `
+```
+
+PMP reads three candidates in order and takes the first one that is present:
-| Attribute | Required | Default | Purpose |
-|------------------------|----------|---------|---------|
-| `data-action-url` | Yes | - | URL invoked on every user choice. `{preference}` is replaced with `standard` or `personalized`. `http(s)` URLs are injected as `