Key takeaways
- Call the Offers API from your server with the app's API key, for one user at a time, with their real country and device.
- Show each offer's reward and requirements exactly as returned, and send users to its start_url unchanged.
- The API only lists offers: rewards still arrive through signed postbacks, and the My offers page still lives on Moonetix.
What is an offerwall API?
An offerwall API returns the offers available to one user as data, usually JSON, so you can show them in your own design instead of embedding a ready-made wall. On Moonetix, your server calls GET https://api.moonetix.com/v1/offers with your app’s API key, renders the offers as cards, and sends users to each offer’s start_url. Rewards still reach you through postbacks, exactly as with the embedded offerwall.
It is the most flexible integration and also the one with the most work. If you are not sure you need it, read Offerwall integration: iframe, link or API? first. The ready-made wall can take your colors, currency name, and icon, which is enough for most sites.
Before you start
- A Moonetix app. Every app has the Offers API next to the hosted offerwall, with the same App ID, keys, and postback, so you can keep the offerwall running while you build, or use both.
- The API key. It is in the app’s Integration tab. It belongs on your server only: never in a browser, a mobile app, or a public repository.
- A live app. Once your publisher account is approved, a new app is live right away. Before approval, or while an app is paused, the API answers
app_not_live. - A postback handler. The API lists offers, it does not credit anyone. See Postback security: verify every reward call.
Step 1: request offers for one user
Send the key as a Bearer token in the Authorization header. The parameters:
| Parameter | Required | Notes |
|---|---|---|
user_id | Yes | The same ID you receive in postbacks. 1 to 100 letters, numbers, and . _ @ : + -. |
country | Yes | Two-letter ISO code of the country the user is really in, such as US. Offers and rewards depend on it. |
device or ua | One of them | desktop, android, or ios, or the user’s user agent. Prefer ua: it also matches offers that target a browser. |
sub1 to sub5 | No | Your tracking values. They are added to every start_url and come back in postbacks. |
A PHP function that fetches and caches one user’s list. APCu is used for the cache here, and Redis or your framework’s cache works the same way:
function moonetix_offers(string $userId, string $country, string $ua): array
{
$key = 'shk_offers_' . md5($userId . '|' . $country . '|' . $ua);
$cached = apcu_fetch($key, $hit);
if ($hit) {
return $cached;
}
$ch = curl_init('https://api.moonetix.com/v1/offers?' . http_build_query([
'user_id' => $userId,
'country' => $country,
'ua' => $ua,
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . MOONETIX_API_KEY],
]);
$data = json_decode((string) curl_exec($ch), true);
if (empty($data['ok'])) {
return ['offers' => [], 'error' => $data['error'] ?? 'unavailable'];
}
apcu_store($key, $data, 120);
return $data;
}
Where does the country come from? From your own IP geolocation, for example a GeoIP database, or a header your CDN adds, such as Cloudflare’s CF-IPCountry when its IP geolocation is on. Do not let users pick a country to see better offers: when a user starts an offer, Moonetix checks the country and device again, and an offer that does not match is not available to them.
Step 2: cache briefly, per user
- 1 to 5 minutes per user is fine. Offers change as budgets run out and new offers start, so a list kept for hours sends users to offers that are gone.
- Never share one user’s list with another. The list is filtered for that user: offers they already completed, or cannot do again yet, are left out. The cache key above includes the user ID for that reason.
- Stay under the rate limit. Each app can make 600 requests per minute, Offers API and SDK session requests together. Above that, the API answers
rate_limitedwith aRetry-Afterheader in seconds. - An empty list is a normal answer. Show a friendly “no offers right now, check back later” message rather than an error.
Step 3: sort and filter
Which offers can appear is set on the Offers page of your dashboard, where you can hide single offers. Hidden offers are left out of the API too, so the API and an embedded wall of the same app return the same offers.
The order and the grouping on your page are up to you. Useful fields for sorting:
| Sort or filter | Field |
|---|---|
| Highest reward first | reward |
| Newest first | added_at (ISO 8601) |
| Fewest steps first | the number of items in goals |
| Tabs by kind | category |
usort($data['offers'], fn($a, $b) => $b['reward'] <=> $a['reward']);
If you sort by a field that can be null, such as reward_max, put those offers at the end rather than treating the value as zero.
Step 4: render offer cards
A card needs the image, the title, the task, the reward in your currency, and a button to start. A minimal template:
<?php foreach ($data['offers'] as $o): ?>
<article class="offer">
<?php if ($o['thumbnail_url']): ?><img src="<?= htmlspecialchars($o['thumbnail_url']) ?>" alt="" width="64" height="64"><?php endif; ?>
<h3><?= htmlspecialchars($o['title']) ?></h3>
<p><?= htmlspecialchars($o['description']) ?></p>
<p class="reward"><?= $o['reward_max'] !== null ? 'Up to ' . $o['reward_max'] : $o['reward'] ?> <?= htmlspecialchars($data['currency']) ?></p>
<a href="<?= htmlspecialchars($o['start_url']) ?>" target="_blank" rel="noopener">Start</a>
</article>
<?php endforeach; ?>
Details that make the difference:
- Escape everything. Titles,
description, and step names are plain text, not HTML. Escape them on output, and show line breaks indescriptionas line breaks. - Show the steps on a detail view. Show
descriptionand listgoals, the steps in order, each with its own reward. Every offer is for new users only, with no VPN and no emulator, so say that once too. - Bonuses and ranges. When
reward_before_bonusis set, you can show it crossed out next toreward. Forrevshareoffers, showreward_maxas “up to”. - Ignore unknown fields. New fields can be added over time, so do not fail on them.
Step 5: start offers with start_url
Each offer has a start_url made for that user. It already carries the user ID and, if link security is on, the hash, so your server does not sign anything here. Open it in a new tab, or in your app’s browser, and do not change it. It opens the offer on a Moonetix page that uses the colors and currency icon from your app’s Design tab, and users who come this way show as Offers API in your statistics.
Only open a start link when the user clicks Start. Do not prefetch it, open it automatically, or hide it in a frame.
Step 6: link to My offers
An offer started from the API is the same as one started on the embedded wall: its status, credited steps, and reversal belong to the user ID and your app. Every response includes a link for the user:
history_urlopens the user’s My offers page on the offerwall, with every offer they started, its status, rewards, and goals, and a Report a missing reward button for rewards that do not arrive.
Put it next to your offer list. Without it, users have no way to follow their offers, and every question becomes a support ticket. Handling missing reward tickets covers the rest.
What not to do with an offerwall API
- Do not change the reward you show.
rewardis exactly what the postback will send as{reward}. Showing more than that is misleading promotion and fills your inbox with complaints. If you want to give users more, change your rate, user split, or bonus in the app’s Currency tab, so the API and the postback agree. - Do not hide or rewrite the terms. Requirements, steps, and time limits decide whether a completion is confirmed. Shortening them to make a card look easier leads straight to rejections.
- Do not call the API from the browser or an app. That exposes the key. If it ever leaks, use New API key in the Integration tab: the old key stops working right away.
- Do not credit on click. A start is not a result.
- Do not keep showing offers to a blocked user. The answer
user_blockedmeans exactly that.
Postbacks still credit users
When a result is final, Moonetix calls your postback URL, with {integration} set to api if you include that macro. Verify the hash, credit status 1 once per transaction ID, take back status 2 only if you credited it, answer with a 2xx status within 6 seconds, and do not redirect. The flow is the same for every integration, so a handler you wrote for an embedded wall needs no change. The full reference is in the Postbacks docs, and every API field is in the Offers API docs, with an OpenAPI file you can import into Postman.
Building on the Moonetix Offers API
The Offers API returns the same offers as your Moonetix offerwall, without the offers you hide on the Offers page, with rewards already converted to your currency and start links already signed for each user. One API key per app also works for the Reporting API, so you can pull your statistics into the same dashboard. Publisher applications are open. See how to get approved as a publisher, then call the Offers API with the API key from your app’s Integration tab.
Frequently asked questions
Can I call the offerwall API from JavaScript in the browser?
No. The API key would be visible to every visitor. Call it from your server and send your own page or JSON to the browser.
How often should I refresh the offer list?
Caching one user’s list for 1 to 5 minutes is fine. Longer than that, users start offers that are no longer available.
Do I need to sign the start links myself?
No. Each start_url already carries the user ID and, when link security is on, the hash. Use it unchanged.
Why does the API return fewer offers than I expected?
Offers depend on the user’s country and device, on what that user already completed, and on any offers you hid on the Offers page.
Can I show a higher reward than the API returns?
No. Show reward as returned, because that is what the postback sends. To give users more, change your rate, user split, or bonus in the app’s Currency tab.


