Appearance
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:
- Calculates the digest with the
application_secretand compares it with thesignature - Checks that the
user_idand theplacement_hashin the signed string are the ones you expect - 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:
| Parameter | Type | Description |
|---|---|---|
application_key | string | The partner-specific application key provided by Infillion |
network_user_id | string | The user_id of the user who completed the ad |
currency_amount | int | The amount of currency earned |
currency_label | string | The label of the currency, for example coins |
revenue | decimal | The revenue you earned for the engagement, with up to 8 decimal places |
placement_hash | string | The hash of the placement of the engagement |
campaign_name | string | The name of the campaign |
campaign_id | string | The id of the campaign |
creative_name | string | The name of the creative |
creative_id | string | The id of the creative |
engagement_id | string | The 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 |
sig | string | The signature of the request |
Your callback URL must respond with one of the following codes:
| Code | Meaning |
|---|---|
0 | Recoverable failure. The request is retried |
1 | The callback was processed |
2 | Invalid signature. Infillion is notified to investigate |
3 | Invalid 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:
- Put all the parameters except
siginkey=valuepairs - Sort the pairs alphabetically by key
- Concatenate the pairs, with no separator
- Append the
application_secret - Calculate the HMAC-SHA1 digest of the string, with the
application_secretas 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
