Key takeaways
- Build the signed offerwall link in PHP for the logged-in user, never in a static block or in JavaScript.
- Keep the link hash salt and the postback secret key in wp-config.php, and show no offerwall to logged-out visitors.
- Credit points only from verified postbacks, once per transaction ID, and answer with a 2xx status within 6 seconds.
How do you add an offerwall to WordPress?
To add an offerwall to WordPress, create a small site-specific plugin with three parts: a shortcode that builds the signed offerwall link for the logged-in user and outputs the iframe, a page that holds the shortcode, and a REST route that receives postbacks and credits points. The secrets stay in wp-config.php on your server, and logged-out visitors get a log-in prompt instead of the offerwall.
This guide builds that for the Sharklio offerwall in under 100 lines of PHP. It assumes your users already have WordPress accounts, because rewards need a stable account to land in.
What you need before you start
| Item | Where it comes from | Why |
|---|---|---|
| An approved app of the Offerwall type | Your Sharklio dashboard, Apps | Registered for your WordPress site’s address, which the embed needs |
| App ID | Next to the app name at the top of the app page | Part of every offerwall link |
| Link hash salt | Integration tab, Keys and security | Signs each user’s link |
| Postback secret key | Integration tab, Keys and security | Verifies the postbacks we send you |
| An https site on your own domain | Your host | The iframe only loads on https pages of the registered website and its subdomains |
| Access to add a plugin | Your host or WordPress admin | The shortcode and the postback route are PHP |
One detail catches people out: this guide needs a WordPress site that can run your own PHP plugin, such as a self-hosted site. Plans that do not allow custom plugins cannot sign links per user, and the iframe only loads on https pages of the website registered for the app. If you are still deciding between an iframe, a link, and the API, read Offerwall integration: iframe, link or API? first.
Step 1: put the keys in wp-config.php
Open wp-config.php and add three lines above the line that says to stop editing. Copy the values from the Integration tab:
define('SHARKLIO_APP_ID', 'your_app_id');
define('SHARKLIO_LINK_SALT', 'your_link_hash_salt');
define('SHARKLIO_POSTBACK_SECRET', 'your_postback_secret_key');
This file runs on the server and is never sent to browsers, so it is the right home for secrets. Do not paste the salt or the secret key into a block, a theme option, a page builder field, or any JavaScript. Anyone who has the salt can open the offerwall as any of your users, and anyone who has the postback secret can fake rewards.
Step 2: create a small site-specific plugin
You could put the code in your theme’s functions.php, but it disappears when you switch themes, and a theme update can overwrite it. A one-file plugin is safer. Create the folder wp-content/plugins/sharklio-wall/ with a file sharklio-wall.php, and start it with a plugin header:
<?php
/*
Plugin Name: Sharklio Offerwall
Description: Offerwall shortcode and postback endpoint.
*/
if (!defined('ABSPATH')) {
exit;
}
Everything below goes in the same file. Activate the plugin under Plugins once the code is complete, because activation also creates the small table from Step 5.
Step 3: a shortcode that signs the link
The offerwall link for each user is https://wall.sharklio.com/{APP_ID}?user_id={USER_ID}&hash={HASH}, where the hash is an HMAC-SHA256 of the user ID with your link hash salt, in lowercase hex. WordPress gives you the logged-in user with wp_get_current_user(), and PHP’s hash_hmac() builds the hash:
function sharklio_wall_link(): string
{
$user = wp_get_current_user();
if (!$user->exists()) {
return '';
}
$userId = (string) $user->ID;
$hash = hash_hmac('sha256', $userId, SHARKLIO_LINK_SALT);
return 'https://wall.sharklio.com/' . SHARKLIO_APP_ID . '?user_id=' . rawurlencode($userId) . '&hash=' . $hash;
}
add_shortcode('sharklio_wall', function () {
$link = sharklio_wall_link();
if ($link === '') {
return '<p><a href="' . esc_url(wp_login_url(get_permalink())) . '">Log in</a> to see offers and earn points.</p>';
}
return '<iframe title="Offerwall" src="' . esc_url($link) . '" style="border:0;width:100%;height:100vh" allow="clipboard-write"></iframe>';
});
Three choices in this code matter:
- The WordPress user ID is the offerwall user ID. It never changes for the same account, it is not an email address, and it fits the allowed characters (1 to 100 letters, numbers, and
. _ @ : + -). Postbacks send it back to you, so crediting is a direct lookup. - No user, no link. A logged-out visitor gets a log-in link, never an offerwall with an empty or shared ID. Rewards earned without an account would have nowhere to go.
- Shortcodes return, they do not echo. WordPress places the returned HTML where the shortcode sits, and
esc_url()keeps the attribute safe.
Step 4: create the earn page
- Add a new page, for example “Earn points”.
- Write a short intro above the offerwall: what users can earn, what points are worth on your site, and that some rewards need review before they arrive.
- Add a Shortcode block and type
[sharklio_wall]. - Publish, log in as a normal user, and open the page.
Why not paste the iframe into a Custom HTML block? That block is static: every visitor gets the same HTML, so it cannot carry a signed link per user. Use it only for static parts of the page.
Check your caching too. The page now contains a link signed for one specific user, so it must never be cached and served to someone else. Many caching setups skip pages for logged-in users, but confirm that yours does, or exclude the earn page from the cache. Give the page its own menu item, and see Offerwall placement: where to put it to earn more for where entry points work best.
Prefer a floating button?
If you would rather not give the offerwall its own page, Sharklio has a Floating button integration: one script tag adds a corner button that opens the offerwall in a pop-up on any page. It is a separate app type, so create an app of the Floating button type and use its App ID and salt. The output then looks like this, added for logged-in users through the wp_footer hook:
add_action('wp_footer', function () {
$link = sharklio_wall_link();
if ($link === '') {
return;
}
echo '<script src="https://wall.sharklio.com/embed.js" async data-button-url="' . esc_url($link) . '" data-button-text="Earn points"></script>';
});
Each app has its own postback secret key, so if you run both a page and a button, verify each postback with the secret of the app it belongs to, for example with a second route. All button options are in Add a floating Earn button to any website.
Step 5: a REST route that credits points
When a result is credited, reversed, pending, or rejected, Sharklio calls your postback URL with a GET request. In the app’s Postback tab, enter:
https://yoursite.com/wp-json/sharklio/v1/postback?user={user_id}&tx={transaction_id}&status={status}&reward={reward}&hash={hash}
If your site uses plain permalinks, the REST API lives at https://yoursite.com/?rest_route=/sharklio/v1/postback instead, followed by &user= and the other parameters. Now add the route and a small table that remembers the last status of each transaction:
register_activation_hook(__FILE__, function () {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta("CREATE TABLE {$wpdb->prefix}sharklio_tx (
tx varchar(191) NOT NULL,
user_id bigint(20) unsigned NOT NULL,
status tinyint(4) NOT NULL,
PRIMARY KEY (tx)
) {$wpdb->get_charset_collate()};");
});
add_action('rest_api_init', function () {
register_rest_route('sharklio/v1', '/postback', [
'methods' => 'GET',
'callback' => 'sharklio_postback',
'permission_callback' => '__return_true',
]);
});
function sharklio_postback(WP_REST_Request $r)
{
global $wpdb;
$tx = (string) $r->get_param('tx');
$user = (string) $r->get_param('user');
$reward = (string) $r->get_param('reward');
$status = (int) $r->get_param('status');
$expected = hash_hmac('sha256', $tx . ':' . $user . ':' . $reward . ':' . $status, SHARKLIO_POSTBACK_SECRET);
if (!hash_equals($expected, (string) $r->get_param('hash'))) {
return new WP_REST_Response(null, 403);
}
$uid = (int) $user;
if ((string) $uid !== $user || !get_userdata($uid)) {
return new WP_REST_Response(null, 200);
}
$t = $wpdb->prefix . 'sharklio_tx';
$n = 0;
if ($status === 1) {
$n = $wpdb->query($wpdb->prepare("INSERT INTO $t (tx, user_id, status) VALUES (%s, %d, 1) ON DUPLICATE KEY UPDATE status = IF(status IN (3, 4), 1, status)", $tx, $uid));
if ($n > 0) {
sharklio_add_points($uid, (float) $reward);
}
} elseif ($status === 2) {
$n = $wpdb->query($wpdb->prepare("UPDATE $t SET status = 2 WHERE tx = %s AND status = 1", $tx));
if ($n > 0) {
sharklio_add_points($uid, -(float) $reward);
}
} elseif ($status === 3) {
$n = $wpdb->query($wpdb->prepare("INSERT IGNORE INTO $t (tx, user_id, status) VALUES (%s, %d, 3)", $tx, $uid));
} elseif ($status === 4) {
$n = $wpdb->query($wpdb->prepare("INSERT INTO $t (tx, user_id, status) VALUES (%s, %d, 4) ON DUPLICATE KEY UPDATE status = IF(status = 3, 4, status)", $tx, $uid));
}
return new WP_REST_Response(null, $n === false ? 500 : 200);
}
function sharklio_add_points(int $uid, float $delta): void
{
global $wpdb;
add_user_meta($uid, 'sharklio_points', 0, true);
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->usermeta} SET meta_value = meta_value + %f WHERE user_id = %d AND meta_key = 'sharklio_points'", $delta, $uid));
wp_cache_delete($uid, 'user_meta');
}
What each part does:
- The hash check comes first. The postback hash is an HMAC-SHA256 of transaction ID, user ID, reward, and status joined with colons, signed with your postback secret key. A call that fails it gets a 403 and changes nothing.
- The table makes every transaction count once. One transaction keeps the same ID for its whole life, and the same event can arrive twice after a retry. The conditional insert credits only when the status really changes to credited, and a reversal only takes points back from a transaction that was credited.
- Points change in one SQL statement, so two postbacks for the same user at the same moment cannot overwrite each other.
- Unknown users get a 200 but no points. That also covers the test postbacks from the dashboard, which use
test_userby default. - A database error answers 500, so the postback is retried instead of lost.
Show the balance anywhere with get_user_meta(get_current_user_id(), 'sharklio_points', true). If you already run a points or loyalty plugin, call its own function for adding points inside sharklio_add_points() instead, and check its developer documentation for the exact name. For the reasoning behind each rule, see Postback security: verify every reward call.
Make sure postbacks can reach WordPress
- Answer fast. Sharklio waits up to 6 seconds for any 2xx answer, and the handler above needs only a few small queries.
- No redirects. Redirects are not followed, so use the final https address of your site, with or without
wwwexactly as your site serves it. - Let the request through. Security plugins, firewalls, and bot protection sometimes block server requests to
/wp-json/or switch off the REST API for visitors who are not logged in. Allow this one route. The Postbacks docs list the addresses our calls come from, but always keep the hash check as well.
Test before you go live
- In the Integration tab, use Test a link with the ID of a WordPress test account and compare it with the link in your page source. They must match exactly.
- Open the earn page logged in and logged out. You should see the offerwall in the first case and the log-in prompt in the second.
- In the Postback tab, set the user ID to your test account’s number and use Send a test postback with status 1. The points should appear once.
- In Reports, Postbacks, open that test row, copy the URL under Details, and open it in your browser. It is the same transaction with a valid hash, so the points must not change again.
- Send a test with status 2. Every test gets a new transaction ID starting with
test_, so this reversal is for a transaction you never credited, and the points must stay as they are. - Change one character of the hash in a copied URL and open it: your route must answer 403. Then remove the test points.
The Testing and troubleshooting page lists every error page, such as Link not valid, with its fix.
Mistakes that cost WordPress sites money
- Crediting points when a user clicks an offer, instead of when a verified status 1 postback arrives.
- A full-page cache that serves one user’s signed link to everyone.
- Mixing up the link hash salt and the postback secret key. They are different values with different jobs.
- Letting people below the minimum age earn. End users must be at least 16, so ask for age at sign-up.
Running the Sharklio offerwall on WordPress
Everything in this guide uses features each Sharklio app has today: a signed link, an iframe embed or a floating button on your registered website, your own currency name and rate, and signed postbacks for every credit and reversal. There is no SDK or WordPress plugin to install from us, just the few lines of PHP above. Publisher applications open soon. Get ready now with how to get approved as a publisher, and read how the Sharklio offerwall works to plan your earn page.
Frequently asked questions
Can I add an offerwall to WordPress without a plugin?
Not safely for logged-in users. The link must be signed per user with a secret salt, and that takes PHP on the server. The site-specific plugin in this guide is the smallest way to do it, and it survives theme changes.
Can I paste the offerwall iframe into a Custom HTML block?
Only a static link, and that would show every visitor the same user ID. Use a Shortcode block with a shortcode that builds the link for the logged-in user.
Why is my offerwall iframe blank on WordPress?
Usually the page is not on the website registered for the app, or it is not served over https. The iframe only loads on https pages of that website and its subdomains, so a staging copy on another domain stays blank.
Where should I store points in WordPress?
User meta works for a simple balance, as shown above, as long as each transaction is recorded once. If you already use a points plugin, credit through that plugin so balances stay in one place.
Does the postback endpoint need a logged-in user?
No. Postbacks come from our server, not from the user’s browser, so the route is public and is protected by the hash check instead.