Skip to content

Reward Verification ​

The onCredit event runs in the user's browser, so a user can fake it. Before you grant a reward, verify the engagement on your server. This page shows how to verify the engagement signature, and how to receive and verify the optional server-side callback.

Engagement Signature ​

The engagement object that your page receives in onCredit has a signature. It is an HMAC-SHA256 digest of the signature_argument_string, calculated with your application_secret. Your page sends the engagement to your server, and your server:

  1. Calculates the digest with the application_secret and compares it with the signature
  2. Checks that the user_id and the placement_hash in the signed string are the ones you expect
  3. Checks that the engagement key was not used before, so the same engagement can't reward the user twice

Keep the secret on the server

Calculating the digest in the browser means shipping your application_secret to the user. Always verify on your server.

The signature_argument_string is a query string with the following keys, sorted alphabetically: completed_at, creative_id, currency_amount, engagement_key, network_user_id, and partner_config_hash. It contains the legacy names of user_id and placement_hash.

js
import { createHmac, timingSafeEqual } from 'node:crypto';

const usedEngagementKeys = new Set(); // use your database in production

function verifyEngagement(engagement, expected) {
  const { signature, signature_argument_string: signed } = engagement;

  // 1. The signature matches
  const digest = createHmac('sha256', process.env.APPLICATION_SECRET)
    .update(signed)
    .digest('hex');
  const a = Buffer.from(digest);
  const b = Buffer.from(String(signature));
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return false;
  }

  // 2. The engagement is for this user and this placement
  const params = new URLSearchParams(signed);
  if (
    params.get('network_user_id') !== expected.userId ||
    params.get('partner_config_hash') !== expected.placementHash
  ) {
    return false;
  }

  // 3. The engagement was not used before
  const key = params.get('engagement_key');
  if (usedEngagementKeys.has(key)) {
    return false;
  }
  usedEngagementKeys.add(key);

  return true;
}

Server-Side Callback ​

Most partners use the client-side onCredit event with the verification above, and it is enough for most use cases. If a client-side callback is not possible, you can use a server-side callback instead. Give Infillion a callback URL during the integration. When a user completes an engagement, Infillion sends a request to the URL, usually in real time, but it can be delayed under load. The request has the following parameters:

ParameterTypeDescription
application_keystringThe partner-specific application key provided by Infillion
network_user_idstringThe user_id of the user who completed the ad
currency_amountintThe amount of currency earned
currency_labelstringThe label of the currency, for example coins
revenuedecimalThe revenue you earned for the engagement, with up to 8 decimal places
placement_hashstringThe hash of the placement of the engagement
campaign_namestringThe name of the campaign
campaign_idstringThe id of the campaign
creative_namestringThe name of the creative
creative_idstringThe id of the creative
engagement_idstringThe unique id of the engagement. If you receive an engagement_id twice, ignore the request and return the failure code 3, so the user is not credited twice
sigstringThe signature of the request

Your callback URL must respond with one of the following codes:

CodeMeaning
0Recoverable failure. The request is retried
1The callback was processed
2Invalid signature. Infillion is notified to investigate
3Invalid user or duplicate engagement_id. The request is not retried

Callback Signing ​

All callbacks are signed to protect both Infillion and publishers from forged requests. To verify the sig:

  1. Put all the parameters except sig in key=value pairs
  2. Sort the pairs alphabetically by key
  3. Concatenate the pairs, with no separator
  4. Append the application_secret
  5. Calculate the HMAC-SHA1 digest of the string, with the application_secret as the key, and encode it in Base64
js
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const app = express();
const seenEngagementIds = new Set(); // use your database in production

function callbackSignature(params, applicationSecret) {
  const base = Object.keys(params)
    .filter((key) => key !== 'sig')
    .sort()
    .map((key) => `${key}=${params[key]}`)
    .join('') + applicationSecret;

  return createHmac('sha1', applicationSecret).update(base).digest('base64');
}

app.get('/truex/callback', (req, res) => {
  const expected = Buffer.from(
    callbackSignature(req.query, process.env.APPLICATION_SECRET)
  );
  const actual = Buffer.from(String(req.query.sig));

  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
    res.send('2'); // invalid signature
    return;
  }

  const { engagement_id: engagementId, network_user_id: userId } = req.query;
  if (seenEngagementIds.has(engagementId) || !isKnownUser(userId)) {
    res.send('3'); // duplicate engagement or unknown user
    return;
  }
  seenEngagementIds.add(engagementId);

  grantReward(userId, Number(req.query.currency_amount));
  res.send('1'); // processed
});

isKnownUser and grantReward are yours.

Security ​

Besides the signature and the engagement_id check, you can block all callback requests except those from the following IP addresses:

50.16.245.107
50.16.245.109
50.16.245.111
184.73.195.45
204.236.225.149
184.73.184.220
174.129.192.205
174.129.193.108

Integration documentation for Infillion Ads