Consent & Withdrawal
Your members agree to share their viewing activity with Rewarded Media on your side, because you own the account relationship. This page covers two ways you pass that decision to us:
- Consent claim: each signed link states which version of your consent disclosure the member accepted.
- Withdrawal: when a member withdraws consent, your server calls us once. We de-identify their viewing history and stop admitting them until they consent again.
ts rules (Unix seconds, valid for the promotion's link expiry).
1. The consent claim
Add a consent parameter to the gateway link. Its value is your label for the disclosure text the member accepted, for example ar-2026-09-v1. Include it in the signed message so it can't be added or changed in transit:
message = mid~secret~ts~consent
sig = HMAC_SHA256(key = secret, message) # lowercase hex
https://www.rewardedmedia.com/api/promo/YOUR_SLUG?mid=USER_ID&ts=UNIX_SECONDS&consent=VERSION&sig=SIG
- A signed consent claim is your statement that the member has consented. Only add it when the member has an active, unexpired consent on record with you. If they haven't consented, don't send them into the promotion.
- We store the version against the member, and against the viewing session it opened, so there's a record of which disclosure each session relied on.
- Links without
consentkeep the originalmid~secret~tsformat. If Require signed consent is on for the promotion (Security tab), those links are refused with403. - With Require signed consent on, signed links are accepted only via
/api/promo/:slug(or the Embed SDK, which goes through it).
If you don't collect consent: our consent screen
On promotions in VPPA mode, a signed link that arrives without a consent claim isn't refused. Instead we ask the member ourselves, before anything about them is stored:
- The gateway verifies your signature as usual, then sends the member to
/promo/YOUR_SLUG/consent. Their member ID is held encrypted for the length of the link expiry, not saved. - The screen explains, in plain language, that we'll use the member ID you sent to track their progress and report rewards back to you. Agreeing takes a separate checkbox that always starts unticked.
- Continue (box ticked): we record their consent and the exact wording they saw, then open the promotion. They aren't asked again on later visits for up to two years, unless the wording changes or you withdraw them.
- No thanks: nothing is stored, our cookies are cleared, and they can't earn from the promotion. If they had records from an earlier visit with no earned points, those are deleted. If they had earned points, the points record stays and their viewing history is de-identified.
The consent screen loads no third-party scripts. The wording can be customized per promotion; ask your Rewarded Media contact. If you'd rather collect consent yourself, send the consent claim above and the screen never appears. With Require signed consent on, links without a claim are refused instead.
2. Withdrawing consent
When a member withdraws consent on your side, make one signed server-to-server call:
POST https://www.rewardedmedia.com/api/promo/YOUR_SLUG/withdraw
Content-Type: application/json
{ "mid": "USER_ID", "ts": "UNIX_SECONDS", "sig": "SIG" }
message = withdraw~mid~secret~ts
sig = HMAC_SHA256(key = secret, message) # lowercase hex
The withdraw~ prefix keeps a withdrawal signature separate from link signatures, so neither can be reused as the other. Sign with the secret of any one of your promotions. The withdrawal applies to that member ID across every promotion in your organization. Form-encoded bodies work too.
What happens
- We record the withdrawal time for the member ID.
- Their viewing records are de-identified: the link between the member and which videos they watched is deleted, while anonymous view counts remain. Open viewing sessions are ended.
- Any browser still holding their Rewarded Media session cookie is disconnected from their member record.
- Points already earned are not affected. Completion records (member ID, points, time) are kept as the financial record behind the member's balance. Only their per-video details, IP address and user agent are removed.
Response
HTTP/1.1 200 OK
{
"status": "withdrawn",
"mid": "USER_ID",
"withdrawn_at": "2026-09-24T15:04:05Z",
"video_views_deidentified": 12,
"sessions_ended": 1,
"transactions_scrubbed": 4
}
The call is idempotent: retrying is safe, and withdrawing a member ID we've never seen still returns 200 and blocks it going forward.
| Status | Meaning |
|---|---|
| 200 | Withdrawn (or already withdrawn). |
| 403 | Signature invalid, ts expired or in the future, or parameters missing. Recompute ts and retry. |
| 404 | Unknown slug, or the promotion has no signing secret. |
After a withdrawal
- Stop signing links for the member. Any link signed at or before the withdrawal is refused, including one already sent out and replayed later.
- If the member consents again, send them in with a link that has a consent claim and a
tsafter the withdrawal. That re-admits them and clears the withdrawal. A link without a consent claim is refused for a withdrawn member, even on promotions that don't normally require one.
Examples
const crypto = require(\'crypto\');
const hmac = (secret, msg) => crypto.createHmac(\'sha256\', secret).update(msg).digest(\'hex\');
const BASE = \'https://www.rewardedmedia.com\';
// Consent-bearing link (only for members with active consent)
function promoLink(slug, mid, consentVersion, secret) {
const ts = Math.floor(Date.now() / 1000).toString();
const sig = hmac(secret, `${mid}~${secret}~${ts}~${consentVersion}`);
const q = new URLSearchParams({ mid, ts, consent: consentVersion, sig });
return `${BASE}/api/promo/${slug}?${q}`;
}
// Withdrawal
async function withdraw(slug, mid, secret) {
const ts = Math.floor(Date.now() / 1000).toString();
const sig = hmac(secret, `withdraw~${mid}~${secret}~${ts}`);
const res = await fetch(`${BASE}/api/promo/${slug}/withdraw`, {
method: \'POST\',
headers: { \'Content-Type\': \'application/json\' },
body: JSON.stringify({ mid, ts, sig }),
});
if (!res.ok) throw new Error(`withdraw failed: ${res.status}`);
return res.json();
}
import hashlib, hmac, time, requests
from urllib.parse import urlencode
BASE = "https://www.rewardedmedia.com"
def sign(secret, msg):
return hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
# Consent-bearing link (only for members with active consent)
def promo_link(slug, mid, consent_version, secret):
ts = str(int(time.time()))
sig = sign(secret, f"{mid}~{secret}~{ts}~{consent_version}")
return f"{BASE}/api/promo/{slug}?" + urlencode(
{"mid": mid, "ts": ts, "consent": consent_version, "sig": sig})
# Withdrawal
def withdraw(slug, mid, secret):
ts = str(int(time.time()))
sig = sign(secret, f"withdraw~{mid}~{secret}~{ts}")
r = requests.post(f"{BASE}/api/promo/{slug}/withdraw",
json={"mid": mid, "ts": ts, "sig": sig}, timeout=10)
r.raise_for_status()
return r.json()
<?php
const RM_BASE = \'https://www.rewardedmedia.com\';
// Consent-bearing link (only for members with active consent)
function promo_link($slug, $mid, $consentVersion, $secret) {
$ts = (string) time();
$sig = hash_hmac(\'sha256\', "$mid~$secret~$ts~$consentVersion", $secret);
return RM_BASE . "/api/promo/$slug?" . http_build_query(
[\'mid\' => $mid, \'ts\' => $ts, \'consent\' => $consentVersion, \'sig\' => $sig]);
}
// Withdrawal
function withdraw($slug, $mid, $secret) {
$ts = (string) time();
$sig = hash_hmac(\'sha256\', "withdraw~$mid~$secret~$ts", $secret);
$ch = curl_init(RM_BASE . "/api/promo/$slug/withdraw");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [\'Content-Type: application/json\'],
CURLOPT_POSTFIELDS => json_encode([\'mid\' => $mid, \'ts\' => $ts, \'sig\' => $sig]),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($code !== 200) throw new RuntimeException("withdraw failed: $code");
return json_decode($body, true);
}
require "openssl"
require "net/http"
require "json"
BASE = "https://www.rewardedmedia.com"
def sign(secret, msg) = OpenSSL::HMAC.hexdigest("SHA256", secret, msg)
# Consent-bearing link (only for members with active consent)
def promo_link(slug, mid, consent_version, secret)
ts = Time.now.to_i.to_s
sig = sign(secret, "#{mid}~#{secret}~#{ts}~#{consent_version}")
query = URI.encode_www_form(mid: mid, ts: ts, consent: consent_version, sig: sig)
"#{BASE}/api/promo/#{slug}?#{query}"
end
# Withdrawal
def withdraw(slug, mid, secret)
ts = Time.now.to_i.to_s
sig = sign(secret, "withdraw~#{mid}~#{secret}~#{ts}")
res = Net::HTTP.post(URI("#{BASE}/api/promo/#{slug}/withdraw"),
{ mid: mid, ts: ts, sig: sig }.to_json,
"Content-Type" => "application/json")
raise "withdraw failed: #{res.code}" unless res.code == "200"
JSON.parse(res.body)
end