Skip to main content

Offer Wall link setup

The Offer Wall email banner opens an Offer Wall that Disco hosts. Behind the banner is one link, and your server builds a new one for every email send. Everything the Offer Wall needs travels inside a single query parameter, p: who the shopper is, which brand sent the email, and the offers request.

This guide covers how to build p. For the banner's design and copy rules, see Build your Offer Wall email banner.

note

The Offer Wall's web address, Disco's key ID and some field names use the platform's original system name, rewarded. They are correct as shown.

1. How it works​

p is a nested JSON Web Token: a signed token (JWS) wrapped inside an encrypted token (JWE). This is called sign-then-encrypt.

p  =  JWE( JWS( payload ) )

The two layers give two separate guarantees:

LayerGuaranteeYou doDisco doesAlgorithm
Inner: JWS (sign)Authenticity. Proves the token came from you and was not altered.Sign with your private signing key.Verify with your public signing key, which you send once.ES256 (EC, P-256 curve)
Outer: JWE (encrypt)Confidentiality. Only Disco can read the contents.Encrypt to Disco's public encryption key (step 2).Decrypt with Disco's private key.ECDH-ES + A256GCM (EC, P-256 curve)

You build p entirely offline. Signing and encryption are local operations, and there is no API call to Disco to create a link.

Private keys never travel. You keep your signing private key, and Disco keeps its decryption private key. Only public keys are exchanged, once, at onboarding.

2. What you need from Disco​

Your key ID (kid). At onboarding, Disco assigns you a namespaced key ID prefix, for example acme. Your signing key's kid then looks like acme:2026-01. It goes into every token you sign so Disco knows which of your keys to verify with. It is a plain label, not a secret.

Your Disco API key. Issued at onboarding. It goes inside the token payload (step 4).

Disco's public encryption key. You encrypt every token to this key. The current production key is:

{
"crv": "P-256",
"kty": "EC",
"x": "I7arbohZiIqb9c6WhuKTBBufnbZavNwgIIx-vQNy1xA",
"y": "zr86hzRPw26r5gqaUAdJyxoki21gK1Q8-rQbJ5B7PpE",
"kid": "rewarded:90ac312d",
"alg": "ECDH-ES",
"use": "enc"
}

Put its kid (rewarded:90ac312d) in your JWE header, so each token names the key it was encrypted to. If Disco rotates this key, you'll be told in advance and given the replacement (see step 9).

3. What you generate and keep​

You generate one signing keypair: an ES256 (EC, P-256) key.

  • Private half: keep it secret in your key store. You sign every token with it, and it never leaves your systems.
  • Public half: send it to Disco once, as a JWK, with the kid you agreed. Disco registers it to verify your tokens.

Your public signing JWK should look like this. Send it without the private d field:

{
"kty": "EC",
"crv": "P-256",
"x": "…",
"y": "…",
"kid": "acme:2026-01",
"alg": "ES256",
"use": "sig"
}

Any JOSE library can generate the pair. Here's an example in TypeScript with jose:

import { generateKeyPair, exportJWK } from "jose";

const { publicKey, privateKey } = await generateKeyPair("ES256", {
extractable: true,
});

const publicJwk = { ...(await exportJWK(publicKey)), kid: "acme:2026-01", alg: "ES256", use: "sig" };
const privateJwk = { ...(await exportJWK(privateKey)), kid: "acme:2026-01", alg: "ES256", use: "sig" };

// Send us `publicJwk`. Store `privateJwk` as a secret.

4. The token payload​

What you sign is a JWT whose body has one data claim, the envelope, alongside the standard JWT claims (jti, iat, exp; see step 5):

{
"data": { // the envelope, nested under `data`
"recommendationsData": { … }, // Required. The recommendations request (see below).
"publisher": {
// Required. Your branding, shown on the Offer Wall.
"name": "Acme Co", // required
"logoUrl": "https://acme.example/logo.png" // optional
},
"apiKey": "…" // Required. The Disco API key we issue you.
}
}

recommendationsData​

The Disco recommendations request. The recommendations endpoint reference lists every field, and step 6 has a complete example.

Fields are camelCase here. The API reference documents fields in snake_case (for example first_name, order_details). Inside the p token, use camelCase (firstName, orderDetails).

publisher​

Required. name is mandatory and logoUrl is optional. This is the brand the shopper sees on the Offer Wall.

apiKey​

Required. The Disco API key issued to you at onboarding.

5. Required claims​

Besides the data payload, set these standard JWT claims when you sign the token:

ClaimWhereRequiredNotes
jtiJWS bodyAlwaysA unique ID for each link. Generate a fresh jti for every send, for example a UUID.
expJWS bodyOne of exp or iatExplicit expiry (Unix seconds). Enforced by Disco.
iatJWS bodyOne of exp or iatIssued-at time. Without exp, the token expires 30 days after iat.
kidJWS headerAlwaysYour namespaced signing key ID, for example acme:2026-01.
algJWS headerAlwaysES256.

A token with neither exp nor iat is rejected, because there is no basis for an expiry. Set at least iat (most libraries do this in one call), and set an explicit exp if you want links to expire sooner than 30 days.

6. Building p, step by step​

Any language with a JOSE library works, because the token is a standard nested JWT. Below is a complete example in TypeScript with jose. It shows the two steps: sign, then encrypt.

import { SignJWT, CompactEncrypt, importJWK, type JWK } from "jose";
import { randomUUID } from "node:crypto";

// Your signing PRIVATE key (from your secret store).
const partnerSigningPrivateJwk: JWK = JSON.parse(process.env.PARTNER_SIGNING_JWK!);

// Disco's PUBLIC encryption key (from step 2).
const rewardedEncryptionPublicJwk: JWK = {
crv: "P-256",
kty: "EC",
x: "I7arbohZiIqb9c6WhuKTBBufnbZavNwgIIx-vQNy1xA",
y: "zr86hzRPw26r5gqaUAdJyxoki21gK1Q8-rQbJ5B7PpE",
kid: "rewarded:90ac312d",
alg: "ECDH-ES",
use: "enc",
};

const payload = {
recommendationsData: {
userDetails: { email: "customer@example.com", firstName: "Sam" },
},
publisher: { name: "Acme Co", logoUrl: "https://acme.example/logo.png" },
apiKey: process.env.DISCO_API_KEY!, // the Disco API key we issue you
};

const signingKey = await importJWK(partnerSigningPrivateJwk, "ES256");
const encryptionKey = await importJWK(rewardedEncryptionPublicJwk, "ECDH-ES");

// 1) SIGN: authenticity + expiry. Fresh `jti` every time.
const jws = await new SignJWT({ data: payload })
.setProtectedHeader({ alg: "ES256", kid: partnerSigningPrivateJwk.kid })
.setJti(randomUUID()) // unique per send
.setIssuedAt()
.setExpirationTime("30d") // or omit for the default 30-day TTL
.sign(signingKey);

// 2) ENCRYPT: confidentiality. Encrypt the JWS to our public key.
const p = await new CompactEncrypt(new TextEncoder().encode(jws))
.setProtectedHeader({
alg: "ECDH-ES",
enc: "A256GCM",
kid: rewardedEncryptionPublicJwk.kid, // "rewarded:90ac312d"
})
.encrypt(encryptionKey);

console.log(`?p=${p}`);

The result is a compact JWE string: five base64url segments separated by dots. That string is what goes in the link.

The exact parameters, for any language​

If you're not using jose, configure your JOSE library to match.

Inner JWS (sign):

  • Header: { "alg": "ES256", "kid": "<your-kid>" }
  • Body: { "data": { … }, "jti": "<uuid>", "iat": <now>, "exp": <now+ttl> }
  • Sign with your P-256 private key.

Outer JWE (encrypt):

  • Header: { "alg": "ECDH-ES", "enc": "A256GCM", "kid": "rewarded:90ac312d" }
  • Plaintext: the compact JWS string from the step above.
  • Encrypt to Disco's P-256 public key.
  • Serialize as compact JWE.

Add the token as the p query parameter on the Offer Wall address:

https://rewarded.disconetwork.com/?p=<the-compact-JWE-string>

p is already URL-safe (base64url). If your email platform or templating layer re-encodes links, make sure p reaches the shopper unchanged: one altered character and the token fails to decrypt.

Put the link behind both the banner image and its call-to-action button, in every email that carries the banner.

8. Testing and going live​

  1. Send Disco your public signing JWK (step 3) with the agreed kid. Disco registers it so your tokens verify.
  2. Build a sample p with the production encryption key (step 6) and a fresh jti.
  3. Open the link: https://rewarded.disconetwork.com/?p=<token>. If the Offer Wall loads with your branding, the integration works end to end.
  4. Send a test email to your Disco contact before your first real send. Together you'll click the banner on desktop and on mobile and confirm the Offer Wall loads with offers. After that, every link is built offline on your side.

Generate a fresh jti for every test as well as every send.

9. Rotating your signing key​

To roll your signing key, for routine rotation or after a suspected compromise:

  1. Generate a new keypair with a new kid, for example acme:2026-02.
  2. Send Disco the new public JWK. Disco registers it alongside the old one, so tokens signed with either key keep verifying during the switch.
  3. Start signing new tokens with the new key.
  4. Once every token signed with the old key has expired (at most 30 days, depending on your expiry), tell Disco to retire the old key.

If Disco rotates its encryption key, you'll receive the new public key and a date to switch by. Tokens encrypted to a retired key stop decrypting.

10. Troubleshooting​

SymptomLikely cause
Token fails to decryptEncrypted to the wrong or an outdated public key, or p was changed in the link. Check you used the current key from step 2 and that p passes through unchanged.
"Unknown signing key"The JWS header kid doesn't match a key Disco has registered. Check the kid matches exactly what you agreed and that your public key is registered.
Signature invalidSigned with a key Disco doesn't hold the public half of, or the payload was altered after signing.
Token expiredexp is in the past, or, with no exp, more than 30 days have passed since iat. Build a fresh token.
"Missing jti"Every token needs a jti. Set one per token.
Payload rejecteduserDetails has no identifier (it needs one of email, emailHash, externalGuid or phone), publisher.name is missing, or apiKey is absent.

Questions?​

Ask your Disco contact. Disco can share a sample token, confirm your public key is registered, or walk through the payload fields you need.