Setting Up Rewarded Ads

Why Use Rewarded Video Ads?

To maximize revenue while maintaining a great UX, Vungle recommends using rewarded video Ad. Shown at natural breaks in the app, rewarded video Ad deliver high revenue, especially if you follow our recommendation to make them non-skippable. This placement also delivers a great UX, because users opt in to watch a video and are rewarded with something of value, such as virtual currency, premium content, or in-app goods. The amount and type of reward given to a user for completing a video view is completely up to you. There are two ways of achieving this: in-app rewards, or server-to-server callbacks. 

Recommended Option: In-App Rewards


This is an alternative to using server-to-server callbacks. When a user successfully completes an ad view or clicks the download button, you can reward them directly in your app. The main benefit of this approach is that it's fairly simple to implement. If you're looking for something quick, and you're not concerned with replay attacks, this should do it.

Vungle now offers a variety of ad formats with Dynamic Template ads. Unlike the traditional ad format, in which an ad play consists of a video play followed by an end card, we offer templates where the call-to-action (CTA) button is available during the video play. The users who complete the video ad, as well as those who click the button, should be rewarded.

Note: Rewarded ads are in some cases referred to as incentivized ads; both terms always refer to the same kind of ad. In the SDK code and in our Reporting API, we use the term 'incentivized'.

Implementation for iOS

Implement the VungleSDK Delegate, discussed in the Get Started with Vungle - iOS SDK v. 6 . 

- (void)vungleDidCloseAdWithViewInfo:(VungleViewInfo *)info placementID:(NSString *)placementID;

If the callback vungleSDKDidCloseAdWithViewInfo passes you a viewInfo dictionary with values of ‘yes’ for the keys completedView or didDownload, the user has earned the reward.

Implementation for Android

Implement the EventListener interface, discussed in the Get Started with Vungle - Android SDK v. 6.

public void onAdEnd (String placementReferenceId, boolean wasSuccessfulView, boolean wasCallToActionClicked)

If the callback onAdEnd returns ‘true’ for either wasSuccessfulView or wasCallToActionClicked, the user has earned the reward.

Server-to-Server Callbacks


Server-to-server callbacks enable you to reward users for ad views with in-game currency or other rewards. When a user successfully completes an ad view, you can configure a callback from Vungle's servers to your own to notify you of the user's completed action.

One benefit of this approach is control. This method allows you to make changes and updates directly on the server side, so there's no need to push an update. Another benefit is security in preventing replay attacks (when a valid data transmission is maliciously or fraudulently repeated or delayed).


Note for Vungle SDK v. 6.x: Starting with SDK v. 6.1, we have moved the rewarded option to the dashboard, so that publishers can easily change the option without a code change. In your dashboard, you just need to create a placement with Rewarded Placement type.

Note for Vungle SDK v.1.0 - v.4.1:

  • For iOS, begin by setting the vunglePlayAdOptionKeyIncentivized option in your playAd object to ‘yes’. 
  • For Android, begin by setting the setIncentivizedFields option in your AdConfig object to ‘true’. 

Implementation for all Vungle SDK Versions

When a user watches 80% or more of a rewarded ad, it is considered a completed view. Vungle will then ping your servers with a callback URL, which looks something like this:

We recommend using etxid and edigest, like this: amount=1&uid=%user%&txid=%txid%&digest=%digest%

Or use txid and digest that has been traditionally offered like this:

Configure the callback URL in your app's Settings on the dashboard (as shown below).


Most publishers only use %user%, %etxid%, and %edigest% for security, but all of the following are available:




The username provided to the Vungle SDK via:

●      iOS: The VunglePlayAdOptionKeyUser key of the options dict passed to playAd()

●      Android: The Vungle.setIncentivizedUserId passed to playAd()


A unique identifier for the device


●      iOS: Apple's unique identifier for the device.

●      Android: This will return the Google Advertiser ID


A unique transaction ID for the completed view with server timestamp; transactionID:timestamp (recommended)


Security token to verify that the callback came from Vungle; refer to the Security section for details (recommended)


A unique transaction ID for the completed view


Security token to verify that the callback came from Vungle; refer to the Security section for details

Note that %user% is the only variable you need to pass in. The rest will come back from Vungle's servers if you include them in the callback URL.

Reward Configuration

Now that you can reward a user for watching an ad, what do you give them? If you're giving out gems every time, it's pretty straightforward. But what if you want to get a bit more advanced? We don't have a built-in option for reward configuration, but here's what we recommend:

Example: Coins vs. Lives

Let's say your app has rewarded ads in multiple places: both in your shop and at every third “game-over.” You want to reward the player with coins in the shop, and with a life at game-over. For each instance of playAd(), configure the user like this: 

userName123:coins or userName123:lives 

Then, when your servers receive Vungle's callback, parse %user% for the correct reward!


Authenticating Callbacks

In order to verify that callbacks you receive originated from Vungle, select the Secret Key for Secure Callback checkbox for your application. This will generate a secret key like this:



You can use the key to verify the origin of the callback as follows:

  1. Create the raw transaction verification string by concatenating your secret key with the transaction ID, separated by a colon, like this:
    transactionString = secretKey + ":" + %txid% or %etxid%
  2. Hash the bytes of the transactionString twice using the SHA-256 algorithm.
  3. Generate the transaction verification token by hex-encoding the output bytes of two sequential rounds of SHA-256 hashing, which will look something like this:
    transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. Check that the transactionToken you generated equals the one sent in the callback query string as %digest% or %edigest%

De-Duplicating Callbacks And Preventing Replay Attacks

There can be events where duplicate callbacks can reach your server due to network issues and replay attacks. To prevent a single callback from being replayed multiple times against your server, store authenticated transaction IDs and reject future callbacks with duplicate transaction IDs.

Using etxid (recommended)

etxid consists of hashed value that is unique for each advertisement event, followed by server side timestamp, therefore the entire string will be also unique.

Using txid

txid consists of hashed value of device ID, followed by device level timestamp, therefore you must incorporate the timestamp to enforce a cutoff line for rewarding.

  1. Extract the timestamp (in milliseconds) from the transaction ID like this: 
    transactionMillis = transactionId.substringAfter(":")
  2. Check that transactionMillis is later than your cutoff and that transactionId has not been encountered since your cutoff.

Sample Code

These bits of sample code will help you implement server-to-server callbacks security! We have examples for: Node.js, Java, Python, and Ruby. Note that:

  • The transaction_id can be %txid% or %etxid% (recommended).
  • For %etxid%, the verification hash should be %edigest%.


var crypto = require('crypto');

function isTransactionValid(secret, transaction_id, provided_hash) {
  return isTransactionRecent(transaction_id) &&
         isTransactionNew(transaction_id)    &&
         createSecurityDigest(secret, transaction_id) === provided_hash;

function getTransactionTimestamp(transaction_id) {
  return parseInt(transaction_id.split(":")[1], 10) || null;

function isTransactionRecent(transaction_id) {
  // Does the transaction have a reasonable format?
  var tx_timestamp = getTransactionTimestamp(transaction_id);
  if (tx_timestamp === null) { return false; }

  // Is the transaction within a reasonable time range?
  var now = new Date().getTime();
  var time_diff = now - tx_timestamp;
  var hour_in_future = -1000 * 60 * 60, three_days_ago = 1000 * 60 * 60 * 24 * 3;
  return (time_diff < three_days_ago && time_diff > hour_in_future);

// Have we seen this transaction before?
// NOTE: To scale beyond just this one node process, you'll need to put this in some
// sort of centralized datastore. Any one that supports atomic insertions into a set
// should do. The Redis database, with its SADD command, would be a good place to start.
var known_transactions = {};
function isTransactionNew(transaction_id) {

// For %etxid%, extract unique event id from etxid, which is the string before “:”, and use it to identify unique ad events
if (known_transactions[transaction_id]) { return false; } known_transactions[transaction_id] = true; return true; } function createSecurityDigest(secret, transaction_id) { var firsthash = crypto.createHash("sha256").update(secret + ":" + transaction_id).digest("binary"); return crypto.createHash("sha256").update(firsthash,"binary").digest("hex"); }


import java.nio.charset.Charset;
import java.text.ParseException;

public class ServerCallbackSecurityExample {
  private static final long MAX_CALLBACK_AGE_MILLIS = 7 * 24 * 60 * 60 * 1000;  // 7 days

   * Checks that a transaction is recent enough, signed with the secretKey, and not a duplicate.
   * @param transactionId the transaction ID.
   * @param secretKey a shared secret.
   * @param verificationDigest the verification digest sent in the callback querystring.
  public boolean isValidTransaction(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return isRecentTransaction(transactionId)
      && isDigestValid(transactionId, secretKey, verificationDigest)
      && isNewTransaction(transactionId);

  protected boolean isRecentTransaction(String transactionId) {
    boolean isRecent = false;
    try {
      final long minCallbackAgeMillis = System.currentTimeMillis() - MAX_CALLBACK_AGE_MILLIS;
      final long transactionMillis = getTransactionMillis(transactionId);
      isRecent = (transactionMillis > minCallbackAgeMillis);
    catch (ParseException exception) {
      // invalid transaction ID format
    return isRecent;

  protected boolean isDigestValid(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return createSecurityDigest(transactionId, secretKey)

  protected boolean isNewTransaction(String transactionId) {
    // TODO if transactionId is new, store the transactionId with its associated transactionMillis
// For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events
return true; } protected long getTransactionMillis(String transactionId) throws ParseException { final int transactionMillisIndex = transactionId.lastIndexOf(":") + 1; try { if (transactionMillisIndex > 0 && transactionMillisIndex < transactionId.length()) { return Long.parseLong( transactionId.substring(transactionMillisIndex)); } else { throw new ParseException("No timestamp in transaction ID", transactionMillisIndex); } } catch (NumberFormatException exception) { throw new ParseException("Invalid transaction ID timestamp", transactionMillisIndex); // invalid timestamp } } protected String createSecurityDigest(String transactionId, String secretKey) throws NoSuchAlgorithmException { final String verificationString = secretKey + ":" + transactionId; final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return toHexString( messageDigest.digest( messageDigest.digest( verificationString.getBytes(Charset.forName("US-ASCII"))))); } protected String toHexString(byte[] bytes) { final StringBuffer hexStringBuffer = new StringBuffer(); for (final byte byt : bytes) { hexStringBuffer.append( Integer.toString((byt & 0xff) + 0x100, 16) .substring(1)); } return hexStringBuffer.toString(); } }


import hashlib, time

def isTransactionValid(secret, transaction_id, input_hash):
  """Returns whether this transaction id / hash should be considered valid"""
  return isTransactionIDRecent(transaction_id) and \
         isTransactionIDNew(transaction_id)    and \
         createSecurityDigest(secret, transaction_id) == input_hash

def getTransactionTimestamp(transaction_id):
  """Will return the unix time (in milliseconds) of this transaction, or None if invalid"""
  parsed = transaction_id.split(":")
    return int(parsed[1]) if len(parsed) == 2 else None
  except ValueError as e:
    return None

def isTransactionIDRecent(transaction_id):
  """Is this transaction within a reasonable time range?"""
  tx_time = getTransactionTimestamp(transaction_id)

  # Handle bad transaction:
  if tx_time is None:
    return False

  # Handle bad transaction times:
  now = int(time.time() * 1000)
  three_days_ago = now - (1000 * 60 * 60 * 24 * 3)
  one_hour_from_now = now + (1000 * 60 * 60)
  return ( three_days_ago < tx_time < one_hour_from_now )

def isTransactionIDNew(transaction_id, known_transaction_ids=set()):
  """Is this a duplicate transaction?
     NOTE: We only use the Python set for simplicity. For better / more centralized solutions,
     you can use any datastore that supports atomic insertion into a set. For starters, try
     Redis with its "SADD" command."""

"""For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events"""
if transaction_id in known_transaction_ids: return False # Else, valid: known_transaction_ids.add(transaction_id) return True def createSecurityDigest(secret, transaction_id): """Will return the string that the security hash should have been""" firsthash = hashlib.sha256() firsthash.update(secret + ":" + transaction_id) secondhash = hashlib.sha256() secondhash.update(firsthash.digest()) return secondhash.hexdigest()


require "openssl"
require "digest/sha2"
require "base64"
require "time"

# just some helper methods, ignore if using Rails
class Fixnum
  SECONDS_IN_DAY = 24 * 60 * 60
  HOURS_IN_DAY = 24 * 60

  def days
    self * SECONDS_IN_DAY

  def hour
    self * 60 * 60

  def ago
    ( - self)

  def from_now
    ( + self)

def transaction_valid?(secret, txid, input_hash)
  transaction_id_recent?(txid) && transaction_id_new?(txid) && create_security_digest(secret, txid) == input_hash

def transaction_timestamp(txid)
  arr = txid.split(":")
  return arr[1].to_i if arr.size == 2

def transaction_id_recent?(txid)
  tx_time = transaction_timestamp(txid)
  return false if tx_time.nil?

  now =
  three_days_ago = 3.days.ago
  one_hour_from_now = 1.hour.from_now
  three_days_ago.to_i < tx_time && tx_time < one_hour_from_now.to_i

# For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events def transaction_id_new?(txid, transactions = []) return false if transactions.include?(txid) transactions << txid return true end def create_security_digest(secret, txid) verification_string = "#{secret}:#{txid}" first_digest = end

Rewarding Users 

Users love rewarded videos, especially when it's a Vungle video. There is no standard answer for this question. It involves looking at the way your game economy works. A developer should reward the user just enough to make the video option attractive, but not enough where the reward would replace an IAP purchase. Placing a Daily View Cap on rewarded videos is also important; it will limit the number of rewards per user. 

Here are a few questions you should ask when deciding the right amount to reward?

  1. What is more valuable to the user: soft or hard currency? Soft currency is cheaper to give away, but users may be more apt to watch a video for an item they can use right away. You can also use the rewarded video as a way to introduce new items to the user, incentivizing them to purchase the IAP later on.
  2. How enticing is the reward to the user? You want to make sure the reward is something that the user actually needs or wants and is not easy to come by.
  3. How does the reward compare to the most common IAP? You will want the reward to be the stepping stone to get that IAP, or something that the user may want, but never buys. 
  4. How does the reward compare to what the user will earn on average by playing the game? If the user earns the same reward amount by just playing a single level, the user may not perceive that the reward is worth 15 seconds of his/her time. 
  5. Should I cap the number of rewards per user per day? Yes. Setting a Daily View Limit can help you control how many free items/coins the user can earn. 

Our Account Management team is also there to help. Email us at

Powered by Creativity Driven by Performance Sign Up Here


Need further assistance, feel free to reach out to us, we’re here to help!

Was this article helpful?