Signing The Request
To send a user into a promotion, your server constructs a signed URL pointing at the promotion gateway. The gateway verifies the signature, sets a session cookie, and 302s the user to the promotion page.
The URL takes this shape:
https://www.rewardedmedia.com/api/promo/YOUR_SLUG?mid=USER_ID&ts=UNIX_SECONDS&sig=HMAC_SHA256
Below is a complete working example, broken into three paired steps. The left column explains why; the right column has the runnable code in your language of choice. Pick a language at the top — every code panel below switches together.
1. Gather your inputs#
Identify the user with a mid (any string up to 255 chars — numeric ID, username, hashed email). Generate a fresh ts in Unix seconds at request time (not milliseconds). Pull your shared secret from server-side storage — never ship it to the client. The param name must be mid; uid, user_id, etc. are rejected.
const crypto = require('crypto');
const mid = user.id.toString();
const ts = Math.floor(Date.now() / 1000);
const sharedSecret = process.env.REWARDEDMEDIA_SECRET;
<?php
$mid = (string) $user->id;
$ts = time();
$sharedSecret = getenv('REWARDEDMEDIA_SECRET');
import hmac, hashlib, time, os
from urllib.parse import quote
mid = str(user.id)
ts = int(time.time())
shared_secret = os.environ['REWARDEDMEDIA_SECRET']
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"os"
"strconv"
"time"
)
mid := userID
ts := strconv.FormatInt(time.Now().Unix(), 10)
sharedSecret := os.Getenv("REWARDEDMEDIA_SECRET")
require 'openssl'
require 'cgi'
mid = user.id.to_s
ts = Time.now.to_i.to_s
shared_secret = ENV.fetch('REWARDEDMEDIA_SECRET')
2. Sign the request#
This is where most integrations break. The signature is HMAC-SHA-256 keyed with your secret over the message mid~secret~ts (tildes between fields). It is not a plain SHA-256(message) — even though the message contains the secret, the plain digest will be rejected. Output must be lowercase hex (not base64, not uppercase). HMAC-SHA-512 is also supported per-promotion.
// Message: mid~secret~ts (tildes between fields)
const message = `${mid}~${sharedSecret}~${ts}`;
// HMAC-SHA-256 keyed with the secret. Lowercase hex.
// NOT crypto.createHash — that's a plain digest, will be rejected.
const sig = crypto.createHmac('sha256', sharedSecret)
.update(message)
.digest('hex');
// Message: mid~secret~ts (tildes between fields)
$message = $mid . '~' . $sharedSecret . '~' . $ts;
// HMAC-SHA-256 keyed with the secret. Lowercase hex.
// NOT hash() — that's a plain digest, will be rejected.
$sig = hash_hmac('sha256', $message, $sharedSecret);
# Message: mid~secret~ts (tildes between fields)
message = f'{mid}~{shared_secret}~{ts}'
# HMAC-SHA-256 keyed with the secret. Lowercase hex.
# NOT hashlib.sha256 — that's a plain digest, will be rejected.
sig = hmac.new(
shared_secret.encode(),
message.encode(),
hashlib.sha256,
).hexdigest()
// Message: mid~secret~ts (tildes between fields)
message := mid + "~" + sharedSecret + "~" + ts
// HMAC-SHA-256 keyed with the secret. Lowercase hex.
// NOT sha256.Sum256 — that's a plain digest, will be rejected.
mac := hmac.New(sha256.New, []byte(sharedSecret))
mac.Write([]byte(message))
sig := hex.EncodeToString(mac.Sum(nil))
# Message: mid~secret~ts (tildes between fields)
message = "#{mid}~#{shared_secret}~#{ts}"
# HMAC-SHA-256 keyed with the secret. Lowercase hex.
# NOT Digest::SHA256.hexdigest — that's a plain digest, will be rejected.
sig = OpenSSL::HMAC.hexdigest('SHA256', shared_secret, message)
3. Compose & send#
Append ?mid=&ts=&sig= to the gateway URL for your promotion's slug. The gateway verifies the signature, sets a session cookie, then 302s the user to the promotion page. Links expire 30 minutes after ts — never cache and replay a signed URL.
const baseURL = 'https://www.rewardedmedia.com/api/promo/your-slug';
const signedURL =
`${baseURL}?mid=${encodeURIComponent(mid)}&ts=${ts}&sig=${sig}`;
// → https://www.rewardedmedia.com/api/promo/your-slug
// ?mid=abc123&ts=1777293741&sig=aa752c82...
res.redirect(signedURL);
$baseURL = 'https://www.rewardedmedia.com/api/promo/your-slug';
$signedURL = $baseURL
. '?mid=' . urlencode($mid)
. '&ts=' . $ts
. '&sig=' . $sig;
// → https://www.rewardedmedia.com/api/promo/your-slug
// ?mid=abc123&ts=1777293741&sig=aa752c82...
header('Location: ' . $signedURL);
base_url = 'https://www.rewardedmedia.com/api/promo/your-slug'
signed_url = (
f'{base_url}?mid={quote(mid)}&ts={ts}&sig={sig}'
)
# → https://www.rewardedmedia.com/api/promo/your-slug
# ?mid=abc123&ts=1777293741&sig=aa752c82...
return redirect(signed_url)
baseURL := "https://www.rewardedmedia.com/api/promo/your-slug"
signedURL := fmt.Sprintf(
"%s?mid=%s&ts=%s&sig=%s",
baseURL, url.QueryEscape(mid), ts, sig,
)
// → https://www.rewardedmedia.com/api/promo/your-slug
// ?mid=abc123&ts=1777293741&sig=aa752c82...
http.Redirect(w, r, signedURL, http.StatusFound)
base_url = 'https://www.rewardedmedia.com/api/promo/your-slug'
signed_url =
"#{base_url}?mid=#{CGI.escape(mid)}&ts=#{ts}&sig=#{sig}"
# → https://www.rewardedmedia.com/api/promo/your-slug
# ?mid=abc123&ts=1777293741&sig=aa752c82...
redirect_to signed_url
Signature debugger
Verify what a signed link should look like, diagnose a hash that isn't validating, or check a built URL for common mistakes — all locally in your browser.
Paste the inputs you used and the sig you computed. We'll byte-compare against the canonical HMAC and ~7 common wrong-variant computations to identify your mistake.
No match against any known variant.
Most likely your secret is wrong, your message construction differs from mid~secret~ts, or your ts doesn't match the one you signed with. Compare your sig to the canonical value below.
Show all variants compared
| Variant | Output |
|---|---|
Paste a full signed URL you built. We'll check param names, timestamp format and age, sig encoding, and other common mistakes.