Signing The Request

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.

Language:

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.

Paste a full signed URL you built. We'll check param names, timestamp format and age, sig encoding, and other common mistakes.