> For the complete documentation index, see [llms.txt](https://help.blings.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.blings.io/role-guides/developer/getting-started/how-to-connect-my-data-to-the-sdk/crm-data-integration/signed-url-tokens-with-public-private-keys.md).

# Protect URL Personalization Data with Signed Tokens

Use a signed token when personalization data must travel in a URL and the Blings landing page must reject values that were changed or created by an untrusted sender. The sending system signs the data with a private key, and the landing page verifies the signature with the matching public key before passing the data to the Blings player.

This pattern is independent of the CRM. The sender can be a CRM server-side function, an automation webhook, middleware, or your application backend, as long as it can create an ECDSA signature.

{% hint style="warning" %}
Signing is not encryption. The token payload is Base64URL-encoded and remains readable to anyone who has the link. The signature provides authenticity and integrity, but not confidentiality. URLs can also appear in browser history, logs, referrer data, and link scanners. Only include fields that your organization has approved for URL use; never include passwords, access tokens, or data that must remain secret.
{% endhint %}

## Before you begin

You need:

* The Dynamic Data field names from your project in Platform Integration.
* A trusted sender-side environment that can securely store a private key and use the Web Crypto API or an equivalent ECDSA library.
* Control of the landing page code, or coordination with Blings to configure the public key on a Blings-hosted landing page.
* A short token lifetime that fits the campaign journey.

Never place the private key in an email template, browser script, URL, mobile app, or public repository. Only the public key belongs on the landing page.

## How the token works

1. The sender builds a JSON payload containing an audience, issue time, expiration time, and the data mapped to the Blings project.
2. The sender converts the JSON to Base64URL and signs that encoded value with the private key.
3. The CRM places the resulting `payload.signature` token in the landing-page URL.
4. The landing page verifies the signature with the public key.
5. The landing page also checks the token audience and expiration time.
6. Only verified personalization data is passed to `BlingsPlayer.create`.

## Generate a key pair once

Run this once in a secure administrative environment. Store the exported private key in your secret manager and provide only the public key to the landing page.

```javascript
const keyPair = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  true,
  ["sign", "verify"]
);

const privateKeyJwk = await crypto.subtle.exportKey(
  "jwk",
  keyPair.privateKey
);

const publicKeyJwk = await crypto.subtle.exportKey(
  "jwk",
  keyPair.publicKey
);

console.log("PRIVATE KEY — store as a secret", privateKeyJwk);
console.log("PUBLIC KEY — safe to share", publicKeyJwk);
```

Each environment should have its own key pair. Do not reuse the example output from another organization or publish the generated private key.

Transfer the generated private JWK directly into your secret manager, then clear it from the console and any temporary files.

## Create the signed token on the sender side

The following JavaScript uses the standard Web Crypto API. Run it only in a trusted server-side environment. Replace the JWK placeholders with the private key generated for your environment.

```javascript
// Store this value in the sender's secret configuration.
// Never include it in browser or CRM message-template code.
const PRIVATE_KEY_JWK = {
  kty: "EC",
  crv: "P-256",
  x: "<private-key-x>",
  y: "<private-key-y>",
  d: "<private-key-d>"
};

const textEncoder = new TextEncoder();

function encodeBase64Url(bytes) {
  let binary = "";

  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }

  return btoa(binary)
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

async function createSignedToken(
  data,
  { audience, expiresInSeconds = 900 }
) {
  if (!audience) {
    throw new Error("A token audience is required");
  }

  if (!data || typeof data !== "object" || Array.isArray(data)) {
    throw new Error("Personalization data must be an object");
  }

  if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
    throw new Error("Token lifetime must be a positive number");
  }

  const now = Math.floor(Date.now() / 1000);
  const payload = {
    version: 1,
    audience,
    issuedAt: now,
    expiresAt: now + expiresInSeconds,
    data
  };

  const privateKey = await crypto.subtle.importKey(
    "jwk",
    PRIVATE_KEY_JWK,
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["sign"]
  );

  const body = encodeBase64Url(
    textEncoder.encode(JSON.stringify(payload))
  );

  const signature = await crypto.subtle.sign(
    { name: "ECDSA", hash: "SHA-256" },
    privateKey,
    textEncoder.encode(body)
  );

  return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;
}
```

The `data` object can contain any fields configured as Dynamic Data in the Blings project. It does not depend on a specific CRM:

```javascript
const personalizationData = {
  preferredLanguage: "en-US",
  serviceRegion: "North",
  featuredTopic: "Account setup",
  contentVariant: "welcome-series"
};

const token = await createSignedToken(personalizationData, {
  audience: "your-experience",
  expiresInSeconds: 15 * 60
});

const landingPageUrl = new URL(
  "https://your-brand.mp5.live/your-experience"
);

landingPageUrl.searchParams.set("d", token);

console.log(landingPageUrl.toString());
```

Use your CRM's merge tags or automation fields to build `personalizationData`. Keep key handling and signing in the trusted sender-side component rather than the message template.

## Verify the token on the landing page

The public key can verify signatures but cannot create them. Replace the JWK placeholders with the public key that matches the sender's private key.

```javascript
const PUBLIC_KEY_JWK = {
  kty: "EC",
  crv: "P-256",
  x: "<public-key-x>",
  y: "<public-key-y>"
};

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

function decodeBase64Url(value) {
  const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);

  return Uint8Array.from(
    atob(padded),
    character => character.charCodeAt(0)
  );
}

async function verifySignedToken(token, { audience }) {
  if (typeof token !== "string") {
    throw new Error("Missing personalization token");
  }

  const parts = token.split(".");

  if (parts.length !== 2 || !parts[0] || !parts[1]) {
    throw new Error("Invalid personalization token format");
  }

  const [body, encodedSignature] = parts;
  const publicKey = await crypto.subtle.importKey(
    "jwk",
    PUBLIC_KEY_JWK,
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["verify"]
  );

  const validSignature = await crypto.subtle.verify(
    { name: "ECDSA", hash: "SHA-256" },
    publicKey,
    decodeBase64Url(encodedSignature),
    textEncoder.encode(body)
  );

  if (!validSignature) {
    throw new Error("The personalization token was modified or forged");
  }

  const payload = JSON.parse(
    textDecoder.decode(decodeBase64Url(body))
  );

  const now = Math.floor(Date.now() / 1000);

  if (payload.version !== 1) {
    throw new Error("Unsupported personalization token version");
  }

  if (payload.audience !== audience) {
    throw new Error("The personalization token has the wrong audience");
  }

  if (!Number.isFinite(payload.expiresAt) || payload.expiresAt <= now) {
    throw new Error("The personalization token has expired");
  }

  if (
    !payload.data ||
    typeof payload.data !== "object" ||
    Array.isArray(payload.data)
  ) {
    throw new Error("The personalization token has no data object");
  }

  return payload.data;
}
```

After verification, pass the returned data to the player:

```javascript
async function initializePersonalizedExperience() {
  const token = new URLSearchParams(window.location.search).get("d");
  const personalizationData = await verifySignedToken(token, {
    audience: "your-experience"
  });

  BlingsPlayer.create({
    project: { id: "project-id-from-platform" },
    settings: {
      container: document.getElementById("blings-video-container")
    },
    data: personalizationData
  });
}

initializePersonalizedExperience().catch(error => {
  console.error("Unable to load personalized experience", error);
  // Show a safe fallback instead of initializing the player with unverified data.
});
```

The sender and landing page must use the same:

* ECDSA P-256 key pair
* SHA-256 signature algorithm
* ECDSA signature encoding: Web Crypto uses a 64-byte IEEE P1363 `r || s` signature for P-256
* Base64URL encoding rules
* URL parameter name, such as `d`
* Audience value
* Payload field names

## CRM implementation options

| Sender capability                                             | Recommended implementation                                                                                                                         |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| CRM supports secure server-side JavaScript and secret storage | Create the token in the CRM's server-side function or automation step.                                                                             |
| CRM can call a webhook but cannot safely store or use the key | Send the mapped fields to trusted middleware, then return the signed URL or token to the CRM.                                                      |
| CRM only supports merge tags in message templates             | Do not put the private key in the template. Use middleware or a backend to create signed links before sending.                                     |
| Blings hosts the landing page                                 | Provide Blings with the public JWK, URL parameter name, audience, and payload field mapping. Keep the private JWK in your own trusted environment. |
| You host the landing page                                     | Verify the token before passing `payload.data` to `BlingsPlayer.create`.                                                                           |

If the sender uses a signing library other than Web Crypto, confirm its ECDSA signature format. Many libraries default to ASN.1 DER. Configure or convert the output to the 64-byte IEEE P1363 format expected by the landing-page code, or update both sides to use the same format.

## Security checklist

* Store the private key in a secret manager and restrict which service can use it.
* Use HTTPS for every landing-page URL.
* Keep token lifetimes short and reject expired tokens.
* Use a different audience for each experience or trust boundary.
* Rotate keys according to your organization's security policy.
* Treat the decoded payload as readable URL data even after its signature is verified.
* Account for URL exposure through browser history, infrastructure logs, referrer data, link scanners, and analytics tools.
* Remember that a valid token can be reused until it expires. Use a short lifetime, or a stateful server-side token exchange when a link must work only once.
* Keep tokens small enough for the URL limits of your CRM, email security tools, browsers, and redirect services.
* Test missing, expired, malformed, and modified tokens before launching the campaign.

If the payload itself must remain confidential, use a server-side token exchange or an encryption design reviewed by your security team. A public-key signature alone does not hide the data.
