Key takeaways
- An IP allowlist is optional: the HMAC signature check is what keeps your postback URL safe, so run it on every call whether or not you check IPs.
- Sharklio postbacks come from 187.124.177.111 (IPv4) and 2a02:4780:79:782c::1 (IPv6), so allow both, and compare parsed addresses rather than strings.
- Behind Cloudflare or any proxy, read the client IP only from a header your trusted proxy sets, and start in log-only mode before you reject anything.
Should you whitelist postback IPs?
You can, but you do not have to, and it never replaces the signature check. Sharklio publishes the two addresses its postbacks come from, 187.124.177.111 (IPv4) and 2a02:4780:79:782c::1 (IPv6), and its docs call an IP check an extra layer: always verify the hash as well. The HMAC signature proves a postback is genuine and unchanged wherever your server sits. An IP allowlist only adds value if your server reads the real sender address correctly, which is exactly what goes wrong behind Cloudflare, load balancers and other proxies.
This guide shows what Sharklio actually sends, where IP allowlists break, and tested code in PHP, Node.js and Python that adds an IP check on top of the signature without cutting off real rewards. If you have not built the signature check yet, start with postback security and come back.
What Sharklio publishes about its postback IPs
The Postbacks docs list the sending addresses and how the requests behave (checked on October 29, 2026):
| Item | Value |
|---|---|
| IPv4 address | 187.124.177.111 |
| IPv6 address | 2a02:4780:79:782c::1 |
| Which one is used | IPv4 whenever your host has one; IPv6 only for hosts without an IPv4 address |
| User agent | Sharklio-Postback/1.0 |
| Request | GET to the URL you saved, on port 80, 443, 8080 or 8443 |
| Your answer | Any 2xx within 6 seconds; redirects are not followed |
| Retries | Up to 5, after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours |
| Address changes | “If these addresses ever change, we tell you in advance.” |
Two things are easy to mix up. The user agent is a label anyone can copy, so it is useful in logs and firewall rules but proves nothing. And the {ip} macro you can add to your postback URL is the IP address the user started the offer from, not the address of the server calling you. Never compare {ip} with your allowlist.
Signature check vs IP allowlist
Each postback can carry a {hash}: the HMAC-SHA256 of the transaction ID, user ID, reward and status, signed with your postback secret key. What is HMAC? explains why only someone with the key can produce it. Here is how the two checks compare:
| Question | HMAC signature | IP allowlist |
|---|---|---|
| Proves the sender knows your secret key | Yes | No |
| Detects a changed user ID, reward or status | Yes | No |
| Works the same behind Cloudflare, a load balancer or a PaaS | Yes, it only reads the URL | Only if you read the client IP correctly |
| Breaks when the sender’s network changes | No | Yes, until you update the list |
| Breaks when your hosting setup changes | No | Often: a new proxy changes what your app sees |
| Cuts noise from scanners hitting the URL | They get a 403 anyway | Yes, before any other work |
| Required on Sharklio | Yes, on every call | No, optional extra layer |
What an IP check adds is a second, independent signal: if a correctly signed postback ever arrives from an address that is not Sharklio’s, either your server reads the IP wrong or your secret key has leaked. That is the best reason to log unlisted addresses even if you never block them.
Why IP allowlists are brittle
Addresses change
Any sender can move servers. Sharklio promises advance notice, but an allowlist hard-coded in three places, or buried in a firewall panel nobody remembers, is how a notice turns into missing rewards. Keep the list in one place, such as a config value or an environment variable.
IPv6 and address formats
Many allowlists only contain an IPv4 address. Sharklio uses IPv6 only when your host has no IPv4 address, but if you move to an IPv6-only host or container, every postback arrives from 2a02:4780:79:782c::1. The same IPv6 address can also be written several ways (2a02:4780:79:782c:0:0:0:1 is the same address), and a Node.js server listening on both protocols reports IPv4 clients as ::ffff:187.124.177.111. A plain string comparison fails on all of these. Parse the addresses and compare the results, as the code below does.
Proxies, CDNs and load balancers
When a proxy sits in front of your app, the connection your app sees comes from the proxy, not from Sharklio. The original address travels in a header such as X-Forwarded-For. Anyone can send that header, so believe it only when the request came through a proxy you trust, and take the address your proxy added, not the first one in the list. Code that reads the leftmost value lets anyone pretend to be any IP, including Sharklio’s. Serverless and PaaS hosts each pass the client IP their own way, so check your platform’s docs for which header it sets and whether it overwrites a client’s value.
Allowlist mistakes that cost real rewards
Checking the IP instead of the hash, allowing only the IPv4 address, reading the leftmost X-Forwarded-For value, comparing with the {ip} macro, enforcing on day one, and forgetting the list when you add Cloudflare or move hosts.
Behind Cloudflare: read the real client IP
With the orange cloud on, Sharklio connects to Cloudflare and Cloudflare connects to your server, so the address your server sees is one of Cloudflare’s. Cloudflare’s HTTP headers reference describes what it sends to your origin (checked on October 29, 2026):
CF-Connecting-IPholds the client IP address connecting to Cloudflare. Cloudflare recommends it because it always contains a single IP address.X-Forwarded-ForequalsCF-Connecting-IPwhen the request had none; if the request already had one, Cloudflare appends the connecting address to it. The earlier values came from the client and prove nothing.True-Client-IPis only available on the Enterprise plan.- With Pseudo IPv4 set to Overwrite Headers, Cloudflare replaces
CF-Connecting-IPandX-Forwarded-Forwith a made-up Class E IPv4 address for IPv6 visitors and keeps the real one inCF-Connecting-IPv6. Its default is Off.
The safe way to use CF-Connecting-IP is to let your web server restore it, and only for connections that come from Cloudflare’s own ranges. Cloudflare’s guide to restoring original visitor IPs uses the nginx realip module, which nginx says is not built by default (run nginx -V and look for --with-http_realip_module). Add one set_real_ip_from line for every range on Cloudflare’s IP list:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 2400:cb00::/32;
real_ip_header CF-Connecting-IP;
After that, PHP’s $_SERVER['REMOTE_ADDR'] is the real client address for requests that came through Cloudflare, and a request that reaches your server directly cannot fake it with a header. On Apache the same guide uses mod_remoteip with RemoteIPHeader CF-Connecting-IP and one RemoteIPTrustedProxy line per Cloudflare range. Cloudflare says its list of ranges needs to be updated regularly, so put that on the same calendar as your allowlist.
If Node.js runs behind that nginx, pass the address on with proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; and set Express’s trust proxy to 'loopback'. The Express guide to running behind proxies explains that it then reads X-Forwarded-For from right to left and stops at the first address it does not trust, so values a client prepends are ignored. If Cloudflare connects straight to Node, give trust proxy Cloudflare’s ranges instead. In Flask, Werkzeug’s ProxyFix does the same job; set x_for to the exact number of proxies in front of you, and use it only if you really are behind one.
Cloudflare bot protection can block postbacks
The more common Cloudflare problem is not spoofing but blocking. Cloudflare’s Bot Fight Mode page says it cannot be bypassed or skipped with WAF custom rules or Page Rules, that it may challenge API traffic, and that it does not trigger if an IP Access rule matches the request first. A server cannot solve a challenge, so a challenged postback simply fails. If you use Bot Fight Mode, either add an IP Access rule that allows Sharklio’s IPv4 address or turn it off for the zone that hosts your postback URL, then confirm in your dashboard under Reports, Postbacks that calls get a 200.
Step by step: signature first, IP check second
Run the IP check after the signature check, with three modes you switch through an environment variable:
off: no IP check.log(the default here): a correctly signed postback from an unlisted address is logged, then processed normally.enforce: a correctly signed postback from an unlisted address gets a 403.
Checking the signature first keeps scanners out of your IP log: every line is a genuine postback seen from an unexpected address. The signature, status and crediting parts below are the example handlers from the Postbacks docs, unchanged; only the IP check is added. Replace the four helper functions with your own storage, as the docs say, and use a ledger with a row lock in production so two copies of one event can never both pay.
PHP
$postbackSecretKey = getenv('SHARKLIO_POSTBACK_KEY');
$sharklioIps = ['187.124.177.111', '2a02:4780:79:782c::1'];
$ipMode = getenv('POSTBACK_IP_MODE') ?: 'log';
function postback_ip_allowed(string $ip, array $allowed): bool
{
$bin = @inet_pton($ip);
if ($bin === false) {
return false;
}
if (strlen($bin) === 16 && strncmp($bin, inet_pton('::ffff:0.0.0.0'), 12) === 0) {
$bin = substr($bin, 12);
}
foreach ($allowed as $a) {
if (inet_pton($a) === $bin) {
return true;
}
}
return false;
}
$tx = $_GET['tx'] ?? '';
$userId = $_GET['user'] ?? '';
$reward = $_GET['reward'] ?? '0';
$status = (int)($_GET['status'] ?? 0);
$expected = hash_hmac('sha256', $tx . ':' . $userId . ':' . $reward . ':' . $status, $postbackSecretKey);
if (!hash_equals($expected, $_GET['hash'] ?? '')) {
http_response_code(403);
exit;
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if ($ipMode !== 'off' && !postback_ip_allowed($ip, $sharklioIps)) {
error_log('postback from unlisted IP ' . $ip . ' tx ' . $tx);
if ($ipMode === 'enforce') {
http_response_code(403);
exit;
}
}
$last = get_last_status($tx);
if ($status === 1 && $last !== 1 && $last !== 2) {
add_to_balance($userId, $reward);
save_status($tx, 1);
} elseif ($status === 2 && $last === 1) {
subtract_from_balance($userId, $reward);
save_status($tx, 2);
}
http_response_code(200);
REMOTE_ADDR is only the real sender if your web server restores it, as in the Cloudflare section above. The ::ffff: branch handles IPv4 addresses that some setups report in IPv6 form.
Node.js (Express)
const crypto = require('crypto');
const net = require('net');
const express = require('express');
const app = express();
app.set('trust proxy', 'loopback');
const sharklioIps = new net.BlockList();
sharklioIps.addAddress('187.124.177.111', 'ipv4');
sharklioIps.addAddress('2a02:4780:79:782c::1', 'ipv6');
const IP_MODE = process.env.POSTBACK_IP_MODE || 'log';
function fromSharklio(ip) {
if (net.isIPv4(ip)) return sharklioIps.check(ip, 'ipv4');
if (net.isIPv6(ip)) return sharklioIps.check(ip, 'ipv6');
return false;
}
app.get('/postback', async (req, res) => {
const { tx = '', user = '', reward = '0', status = '', hash = '' } = req.query;
const expected = crypto.createHmac('sha256', process.env.SHARKLIO_POSTBACK_KEY)
.update(`${tx}:${user}:${reward}:${status}`)
.digest('hex');
const given = Buffer.from(String(hash));
const wanted = Buffer.from(expected);
if (given.length !== wanted.length || !crypto.timingSafeEqual(given, wanted)) {
return res.sendStatus(403);
}
const ip = req.ip || '';
if (IP_MODE !== 'off' && !fromSharklio(ip)) {
console.warn('postback from unlisted IP', ip, 'tx', tx);
if (IP_MODE === 'enforce') return res.sendStatus(403);
}
const s = Number(status);
const last = await getLastStatus(tx);
if (s === 1 && last !== 1 && last !== 2) {
await addToBalance(user, reward);
await saveStatus(tx, 1);
} else if (s === 2 && last === 1) {
await subtractFromBalance(user, reward);
await saveStatus(tx, 2);
}
res.sendStatus(200);
});
Node’s net.BlockList matches IPv4 addresses written in IPv6 form when you check them as ipv6, so ::ffff:187.124.177.111 passes without extra code. The Node.js postback guide covers the full module, the database table and a Next.js version.
Python (Flask)
import hmac, hashlib, ipaddress, os
from flask import Flask, request, abort
app = Flask(__name__)
SHARKLIO_IPS = {
ipaddress.ip_address("187.124.177.111"),
ipaddress.ip_address("2a02:4780:79:782c::1"),
}
IP_MODE = os.environ.get("POSTBACK_IP_MODE", "log")
def from_sharklio(addr):
try:
ip = ipaddress.ip_address(addr or "")
except ValueError:
return False
if ip.version == 6 and ip.ipv4_mapped:
ip = ip.ipv4_mapped
return ip in SHARKLIO_IPS
@app.get("/postback")
def postback():
tx = request.args.get("tx", "")
user = request.args.get("user", "")
reward = request.args.get("reward", "0")
status = request.args.get("status", "")
expected = hmac.new(os.environ["SHARKLIO_POSTBACK_KEY"].encode(), f"{tx}:{user}:{reward}:{status}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.args.get("hash", "")):
abort(403)
ip = request.remote_addr or ""
if IP_MODE != "off" and not from_sharklio(ip):
app.logger.warning("postback from unlisted IP %s tx %s", ip, tx)
if IP_MODE == "enforce":
abort(403)
s = int(status or 0)
last = get_last_status(tx)
if s == 1 and last not in (1, 2):
add_to_balance(user, reward)
save_status(tx, 1)
elif s == 2 and last == 1:
subtract_from_balance(user, reward)
save_status(tx, 2)
return "OK", 200
Python’s ipaddress module raises ValueError for anything that is not an address and exposes ipv4_mapped for the ::ffff: form. The Python postback guide has the Django version and the database side.
We ran all three handlers locally: signed calls were credited once in log mode, a changed reward got a 403 in every mode, unlisted addresses got a 403 in enforce mode, and behind nginx with real_ip_header CF-Connecting-IP a fake X-Forwarded-For could not pass as Sharklio.
How not to lock yourself out
- Ship in
logmode for at least a week of real traffic, including both credits and reversals. - Read the log. No lines means your server sees
187.124.177.111as it should. Lines showing a private address,127.0.0.1or a Cloudflare range mean your proxy setup is not restoring the client IP yet; fix that, not the allowlist. - Switch to
enforce, then watch Reports, Postbacks in your dashboard for a day. Every attempt is listed with the status code your server returned, so a wave of 403s shows up quickly. - Know your safety net. A rejected postback is retried up to 5 times over roughly eight and a half hours. If you fix the problem after that, resend the failed postbacks from the report, and handle repeats by transaction ID so a resend never pays twice.
- Keep the switch. When you change hosts, add a proxy, or get an address change notice, set the mode back to
logfirst, then update and enforce again. - Scope it to the postback path. If you allowlist in a firewall or web server instead of the app, apply it only to the postback URL, never to the whole site, and make sure blocked requests are logged somewhere you look.
A useful alert
Alert on any log line from a correctly signed postback with an unlisted address. In a working setup it should never fire. If it does and the address is not one of your own proxies, treat it as a leaked postback secret key and replace the key from the Integration tab.
Test the allowlist before you enforce it
- Send a test postback from Integration status on the Integration tab. It goes out through the same sender as real postbacks and is signed the same way, with a
transaction_idthat starts withtest_. Inlogmode, check that no unlisted-IP line was written; inenforcemode, check that your server still answered 200. - Call your URL yourself with a correctly signed test request from your own computer. In
enforcemode it should get a 403, which proves the IP check runs. Use a test user and atest_transaction ID so nothing real is credited. - Try a fake header. Send the same request with
X-Forwarded-For: 187.124.177.111orCF-Connecting-IP: 187.124.177.111, straight to your server if it is reachable without Cloudflare. It must still get a 403. If it gets a 200, your server trusts a header it should not.
The Testing and troubleshooting docs list the usual postback errors, and test your app before launch walks through the whole pre-launch check.
How Sharklio postbacks fit this setup
Sharklio is a pay-per-result affiliate network, and every credit and reversal reaches your server as a signed server-to-server postback from two published addresses, with a test button and a Postbacks report that shows each attempt and your server’s answer. See how the wall works, read the Postbacks reference, or get in touch.
Frequently asked questions
Should I whitelist postback IPs?
It is optional. Always verify the HMAC signature on every postback; add an IP allowlist only as an extra layer, and start it in log-only mode so a wrong setup cannot block real rewards.
What IP addresses do Sharklio postbacks come from?
Sharklio’s Postbacks docs list 187.124.177.111 (IPv4) and 2a02:4780:79:782c::1 (IPv6). IPv4 is used whenever your host has an IPv4 address, and Sharklio says it will announce any change in advance.
Is an IP whitelist enough to secure a postback URL?
No. An IP check cannot tell whether the user ID or reward was changed, and a proxy that is set up wrong can let anyone pass it. The signature check is the reliable method.
Why does my server see a Cloudflare IP instead of the postback sender?
Cloudflare proxies the request, so the connection comes from its network. Restore the real address from CF-Connecting-IP in your web server, trusting that header only from Cloudflare’s published ranges.
What happens if my allowlist blocks a real postback?
Sharklio treats any non-2xx answer as a failure and retries up to 5 times over about eight and a half hours. After that, fix the cause and resend the postback from Reports, Postbacks.
Do I need to allow the IPv6 address too?
Yes. It is only used for hosts without an IPv4 address, but allowing both means a move to an IPv6-only server does not silently break your rewards.



