Server-to-Server Postbacks (v2)
Server-to-server postbacks deliver conversion events to your server using a GET request. By default, AdGem is set to send postbacks via GET request unless otherwise enabled by your Publisher Support Advocate.
By default, AdGem only sends postbacks on Payable Conversion events. If you would like to enable Install postbacks to be sent to you, please reach out to your dedicated Publisher Support Advocate so we can enable this for you!
Postback Types
Reward / Conversion Postbacks
By default, AdGem sends a postback for a rewarded player conversion event. AdGem sends this postback to the Postback URL set in the AdGem dashboard.
Install Postbacks
For many of AdGem's offer campaigns we have the ability to send a postback to you when your players install an app from an offer. This allows you to better track the offer's performance with your players on your platform.
An install postback from AdGem follows the same Postback URL string schema as you have placed in the AdGem dashboard. For GET request postbacks, AdGem automatically appends &nonpayable_install_goal=true to the end of the string.
Setting Up Your Postback
Enabling Server Postback
From the AdGem Dashboard navigate to Properties & Apps and choose Edit from the Options drop down menu.

Once on the Properties & Apps menu, scroll down until you see Postback Options. Click on the radio input next to Server Postback.

Several new fields will appear that will allow you to configure your server postback.
Postback Key
The first field is your Postback Key. Copy this key to a secure place as you will need it to implement Postback Hashing. For security reasons, the Postback Key will only be visible once. See the Securing Your Postbacks section below to learn more about this important feature.
Postback URL
Next, you will need to provide a Postback URL hosted on your server. AdGem will send a GET request to the URL you provide every time a conversion occurs from your traffic sources.

The following macros are available for use in your postback URL. AdGem's server will replace these with actual values when sending the postback.
All field values are URL encoded using PHP rawurlencode() RFC 3986 (which differs from JavaScript encoding). For example:
A {campaign_name} of Example App: Sports & Casino - CPE FTD (iOS, INCENT, Free, UK) will be returned as Example%20App%3A%20Sports%20%26%20Casino%20-%20CPE%20FTD%20%28iOS%2C%20INCENT%2C%20Free%2C%20UK%29
- Include
{payout}- Track the accurate decimal revenue earned for each conversion - Include
{goal_id}- Track player progress on multi-reward offers (each goal has a unique ID) - Alphabetize parameters - Some frameworks sort query strings, so alphabetizing avoids authentication issues
| Macro | Recommended | Description |
|---|---|---|
{transaction_id} | ✓ | The unique AdGem ID of the offer conversion |
{campaign_id} | ✓ | The unique ID of the campaign/offer that was completed |
{payout} | ✓ | The decimal amount of revenue earned from the user completing the offer |
{amount} | ✓ | The amount of virtual currency to reward the user (can be empty or null) |
{goal_id} | ✓ | The unique ID of each goal in a campaign/offer that was completed |
{player_id} | The unique ID of the player/user on your system | |
{app_id} | The unique ID for your app on AdGem | |
{offer_id} | The unique offer version with which the user engaged (Offer API integrations only) | |
{offer_name} | The name of the campaign/offer as shown in the Offerwall/API | |
{goal_name} | The name of the completed goal | |
{store_id} | The unique ID assigned by Google Play Store or Apple App Store | |
{tracking_type} | The type of offer completed (e.g., CPI, CPE, CPA, CPC, Market Research) | |
{ad_type} | The type of ad unit where the click originated (e.g., offerwall) | |
{all_goals_completed} | Indicates if all goals have been completed for the campaign | |
{allow_multiple_conversions} | Boolean indicating if the offer allows multiple conversions | |
{click_datetime} | The date and time when the user clicked on the offer (EST) | |
{conversion_datetime} | The date and time when the user completed the offer (EST) | |
{c1} | Custom parameter value set by the publisher ^ | |
{c2} | Custom parameter value set by the publisher ^ | |
{c3} | Custom parameter value set by the publisher ^ | |
{c4} | Custom parameter value set by the publisher ^ | |
{c5} | Custom parameter value set by the publisher ^ | |
{country} | ISO country code for the user ^ | |
{state} | The user's state or region ^ | |
{ip} | The IP address of the user ^ | |
{platform} | The platform the device is using (e.g., ios) ^ | |
{os_version} | The user's Operating System version ^ | |
{app_version} | The version of your app where the click originated ^ | |
{gaid} | Google Advertising ID (when using Google Play Services) ^ | |
{idfa} | Apple Advertising ID (when user has not limited ad tracking) ^ | |
{useragent} | The User Agent from the user's default browser ^ |
^ These macros contain data provided by the Publisher in the click_url. AdGem passes these values back in the postback.
Securing Your Postbacks
We provide two security measures for securing postback communication between AdGem and your server:
- IP Whitelisting - Verify the postback comes from AdGem's static IP
- Postback Hashing - Cryptographic verification of the postback integrity
We strongly recommend implementing both security measures to ensure postbacks are genuine. Without proper security, malicious actors could send fake postback requests to fraudulently earn virtual currency.
IP Whitelisting
Postbacks are sent from a static IP address. Whitelist this IP on your server. Contact support@adgem.com or your Publisher Support Advocate to get the production IP address.
Postback Hashing (v2 - GET Requests)
When you enable postback hashing by generating a Postback Key, you'll receive two new query parameters:
request_id- A unique UUID for each postback (no duplicates)verifier- A cryptographic hash for validation
Verification Process:
- Remove the
verifierparameter from the postback URL - Hash the remaining URL using HMAC-SHA256 with your postback secret key
- Compare the calculated hash with the
verifierparameter
- PHP
- Node.js
- Ruby
- Python
hash_hmac('sha256', $postback_url, $postback_secret_key)
Full Example (PHP 7.1+):
// securely supply credentials using environment variables
define('ADGEM_WHITELIST_IP', $_ENV['ADGEM_WHITELIST_IP']);
define('ADGEM_POSTBACK_KEY', $_ENV['ADGEM_POSTBACK_KEY']);
// verify the static IP
if(ADGEM_WHITELIST_IP !== $_SERVER['REMOTE_ADDR']) {
http_response_code(403);
exit('Error: '.$_SERVER['REMOTE_ADDR'].' does not match the whitelisted IP address.');
}
// get the full request url
$protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http");
$request_url = "$protocol://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
// parse the url and query string
$parsed_url = parse_url($request_url);
parse_str($parsed_url['query'], $query_string);
// get the verifier value
$verifier = $query_string['verifier'] ?? null;
if (is_null($verifier)) {
http_response_code(422);
exit("Error: missing verifier");
}
// rebuild url without the verifier
unset($query_string['verifier']);
$hashless_url = $protocol.'://'.$parsed_url['host'].$parsed_url['path'].'?'.http_build_query($query_string, "", "&", PHP_QUERY_RFC3986);
// calculate the hash and verify it matches
$calculated_hash = hash_hmac('sha256', $hashless_url, ADGEM_POSTBACK_KEY);
if ($calculated_hash !== $verifier) {
http_response_code(422);
exit('Error: invalid verifier');
}
// valid - safe to process the postback
http_response_code(200);
exit('OK');
CryptoJS.HmacSHA256(postbackUrl, postbackSecretKey).toString(CryptoJS.enc.Hex)
Full Example:
const http = require('http');
const { URL } = require('url');
const CryptoJS = require("crypto-js");
http.createServer(function (req, res) {
// verify the static ip
let ip = req.socket.remoteAddress;
if (ip != process.env.ADGEM_WHITELIST_IP) {
res.writeHead(422, { 'Content-Type': 'text/html' });
return res.end('Error: ' + ip + ' does not match the whitelisted IP address.');
}
// get the full request url
let protocol = (req.connection.encrypted ? 'https': 'http');
let requestUrl = new URL(protocol + '://' + req.headers.host + req.url);
// get the verifier value
let verifier = requestUrl.searchParams.get('verifier');
if (verifier == undefined) {
res.writeHead(422, { 'Content-Type': 'text/html' });
return res.end("Error: missing verifier");
}
// remove the verifier
requestUrl.searchParams.delete('verifier');
// calculate the hash and verify it matches
let hash = CryptoJS.HmacSHA256(requestUrl.href, process.env.ADGEM_POSTBACK_KEY).toString(CryptoJS.enc.Hex);
if (hash !== verifier) {
res.writeHead(422, { 'Content-Type': 'text/html' });
return res.end("Error: invalid verifier");
}
// valid - safe to process the postback
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('OK');
}).listen(process.env.PORT, '0.0.0.0');
OpenSSL::HMAC.hexdigest("SHA256", postback_secret_key, postback_url)
Full Example (Rails):
class PostbacksController < ApplicationController
before_action :check_ip
def store
if params[:verifier].blank?
logger.error "verifier is blank in url: #{request.original_url}"
return render status: 403, json: {'success': false}
end
# remove verifier from url and keep order of params
hashless_url = request.original_url.split('&verifier=')[0]
calculated_hash = OpenSSL::HMAC.hexdigest("SHA256", ENV['POSTBACK_SECRET_KEY'], hashless_url)
if calculated_hash != params[:verifier]
return render status: 403, json: {'success': false}
end
# valid - safe to process the postback
return render status: 200, json: {'success': true}
end
private
def check_ip
unless request.remote_ip == ENV['ADGEM_WHITELIST_IP']
logger.error "#{request.remote_ip} is not whitelisted"
return render status: 403, json: {'success': false}
end
end
end
hmac.new(postback_secret_key.encode('utf-8'), postback_url.encode('utf-8'), hashlib.sha256).hexdigest()
Full Example:
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs, urlencode
import hmac
import hashlib
class PostbackHandler(BaseHTTPRequestHandler):
def do_GET(self):
host = self.headers.get('Host')
path = urlparse(self.path).path
query_params = parse_qs(urlparse(self.path).query)
# get and remove verifier
verifier = query_params.get('verifier', [None])[0]
if 'verifier' in query_params:
del query_params['verifier']
# rebuild url without verifier
query_string = urlencode(query_params, doseq=True)
url_without_verifier = f"http://{host}{path}"
if query_string:
url_without_verifier += f"?{query_string}"
# calculate HMAC
encoded_key = hmac.new(
ADGEM_POSTBACK_KEY.encode('utf-8'),
url_without_verifier.encode('utf-8'),
hashlib.sha256
).hexdigest()
if encoded_key == verifier:
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
else:
self.send_response(500)
self.end_headers()
self.wfile.write(b"Invalid verifier")
Signature Verification (v3 - POST Requests)
For v3 POST postbacks, verify the request by hashing the request body with your secret key using HMAC-SHA256 and comparing it to the Signature header.
- PHP (Laravel)
- Node.js (Express)
Route::post('/postbacks/adgem/v3', function () {
$receivedSignature = request()->headers->get('Signature', '');
$expectedSignature = hash_hmac('sha256', request()->getContent(), env('ADGEM_POSTBACK_KEY'));
if (hash_equals($expectedSignature, $receivedSignature)) {
return response()->noContent(200);
} else {
return response()->noContent(401);
}
});
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
app.post('/postbacks/adgem/v3', (req, res) => {
const receivedSignature = req.get('Signature') ?? '';
const expectedSignature = crypto
.createHmac('sha256', process.env.ADGEM_POSTBACK_KEY)
.update(req.rawBody)
.digest('hex');
// Compare hex strings as UTF-8 buffers to avoid silent
// truncation from Buffer.from(str, 'hex') on malformed input
const expected = Buffer.from(expectedSignature, 'utf8');
const received = Buffer.from(receivedSignature, 'utf8');
const isValid =
expected.length === received.length &&
crypto.timingSafeEqual(expected, received);
if (isValid) {
res.status(200).send();
} else {
res.status(401).send();
}
});