Publisher documentation

Adspiritmedia gives your app or website an offerwall: your users complete offers (installs, sign-ups, surveys…), the advertiser pays us, and we credit your account and tell your server to reward the user.

  1. Create an app in the dashboard and get your App ID and Postback Password.
  2. Show the offerwall to your users with the iframe / web link, or build your own list with the Offers API.
  3. Set a postback URL so your server is told when a user completes or loses an offer.
  4. Your earnings build up in your balance and are paid out by invoice.

Getting started

1. Account

Sign up, verify your email address, and wait for your account to be approved. Until then the dashboard shows a "pending approval" page.

2. Create an app

Go to Apps → Create app and fill in:

FieldNotes
PlatformAndroid, iOS or Web/Desktop. Can't be changed later.
Property nameYour app or site name (max 255 characters).
Property URLStore link or website URL. Can't be changed later.
Traffic descriptionShort description of your traffic across countries and devices (max 255 characters).
Postback URLOptional at creation; see Postbacks.
3. App approval

Your first app is active straight away. Any app you add after that starts as Pending until our team reviews it. The offerwall and API only work for Active apps.

StatusMeaning
ActiveLive: offers are shown and conversions are paid.
PendingWaiting for review. The offerwall returns App Pending.
RejectedNot accepted. Contact support for details.
DisabledSwitched off by our team.
4. Your keys

Open the app (Apps → Edit) to find:

  • App ID (Property/App ID): used in every integration link.
  • Postback Password: a 32-character secret used to sign postbacks. Generate New replaces it straight away, so update your server at the same time.
  • API Key: only after your API access request is approved (see Offers API).

Currency & points rate

Each app has a Points Rate: how many units of your in-app currency a user gets per 1 USD you earn. The default is 1000 and the minimum is 1. Change it under Apps → Edit → Integration Information.

Formula
reward = payout × points rate

payout 0.50 USD, rate 1000  →  reward 500
  • payout is your share in USD, after our revenue share. It's the amount added to your balance.
  • reward is what you give your user, in your currency. It's shown on the offerwall and sent in the postback.
  • A new rate applies to offers shown and conversions credited after you save it.

Offerwall (iframe / web)

The quickest integration: open our hosted offerwall for a user. Replace {app_id} with your App ID and {user_id} with that user's ID in your system.

Offerwall link
https://adspiritmedia.com/app/iframe/{app_id}/{user_id}
Full-screen iframe (website)
HTML
<iframe
  src="https://adspiritmedia.com/app/iframe/{app_id}/{user_id}"
  style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%;
         border:none; margin:0; padding:0; overflow:hidden; z-index:999999;">
</iframe>
Mobile apps

Open the same link in a WebView or in the device browser. Offer links must be opened on the user's own device, because country, device and VPN are checked when the user clicks.

Android (Kotlin)
val url = "https://adspiritmedia.com/app/iframe/$appId/" + Uri.encode(userId)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.loadUrl(url)
The user ID
  • Use a stable, unique ID for each user. It must not be empty or 0.
  • It's sent back to you unchanged in the postback as {user_id}, so you know whom to reward.
  • Keep it URL-safe (letters, numbers, -, _). Don't use an email address or anything private.
Errors

If the app isn't active the link returns JSON instead of the offerwall, for example {"error":"App Pending"}. Possible values: App Not Found, App Pending, App Rejected, App Disabled, App Deleted.

Offers API

Use the API if you want to show offers in your own design. It's off by default.

Request access

Open Apps → Edit → API, explain how you'll use the API and submit. Once approved, your API Key appears on the same tab. Generate New replaces the key straight away.

Request
GET
https://adspiritmedia.com/api/v1/offers/{app_id}/{user_id}?api={api_key}&ip={user_ip}&selected_platform={platform}
ParameterRequiredDescription
{app_id}YesYour App ID (in the path).
{user_id}YesYour user's ID (in the path). Same rules as the offerwall user ID.
apiYesYour API Key.
ipYesThe user's IP address, not your server's. It decides which country's offers are returned and is checked for VPN/proxy.
selected_platformNoios or android to return only those offers. Leave it out for all offers.
Examples
cURL
curl "https://adspiritmedia.com/api/v1/offers/12345/user42?api=YOUR_API_KEY&ip=203.0.113.7&selected_platform=android"
PHP
$appId  = 12345;
$apiKey = 'YOUR_API_KEY';
$userId = 'user42';

$url = 'https://adspiritmedia.com/api/v1/offers/' . $appId . '/' . rawurlencode($userId) . '?' . http_build_query([
    'api' => $apiKey,
    'ip'  => $_SERVER['REMOTE_ADDR'], // the user's IP
]);

$data = json_decode(file_get_contents($url), true);

if (isset($data['error'])) {
    // e.g. "Invalid API Key", "VPN Detected"
} else {
    foreach ($data['offers'] as $offer) {
        echo $offer['name'] . ' - ' . $offer['reward'] . ' points' . PHP_EOL;
    }
}
Node.js (18+)
const params = new URLSearchParams({ api: 'YOUR_API_KEY', ip: userIp });
const res = await fetch(`https://adspiritmedia.com/api/v1/offers/12345/${encodeURIComponent(userId)}?${params}`);
const data = await res.json();

if (data.error) {
  console.error(data.error);
} else {
  for (const offer of data.offers) console.log(offer.name, offer.reward);
}
Response
200 OK
{
  "offers": [
    {
      "id": 1374,
      "offer_type": "CPI",
      "name": "FAB Mobile Banking - iOS",
      "instructions": "Open and use the app.",
      "description": "Open and use the app.",
      "short_description": "Open and use the app.",
      "disclaimer": "This offer rewards within 24 hours. New users only.",
      "payout": "0.12",
      "reward": "120.00",
      "image": "https://.../offer.jpeg",
      "countries": ["EG", "MY"],
      "goals": null,
      "os": "ios",
      "incent": "0",
      "favorite": 0,
      "url": "https://.../api/redirect/..."
    }
  ]
}
FieldDescription
idOffer ID.
offer_typeCPI, CPA, PIN, CPE, CPS, CPC, Survey or other.
name, description, short_description, instructions, disclaimerTexts to show the user.
payoutYour earning in USD (string, 2 decimals). For offers with goals it's the sum of all goals.
rewardpayout × points rate, in your currency (string, 2 decimals).
imageOffer image URL.
countriesISO-2 country codes the offer accepts (empty or WW = worldwide).
goalsnull, or a list of steps, each with its own payout and reward.
osios, android or all (web / all devices).
incentIncentive flag from the advertiser.
urlTracking link for this user. Send the user's browser there; don't call it from your server.
Errors

Errors come back as HTTP 500 with a JSON body:

500
{"error": "Invalid API Key"}
errorCause
Missing parametersApp ID or user ID missing.
App Not Found, App Pending, App Rejected, App Disabled, App DeletedThe app isn't active, or the API request is still pending / was rejected.
App Not ApprovedAPI access was never requested for this app.
Invalid API KeyWrong or old key (check after Generate New).
Invalid IPThe ip parameter is missing.
VPN DetectedThe user's IP is a VPN or proxy (see VPN & fraud rules).

Clicks & redirects

Every offer link (on the offerwall or the API's url) goes through our redirect, which records a click and sends the user to the advertiser. Before redirecting we check:

  • the app is active and the offer is still active and not disabled;
  • the user's country is one the offer accepts;
  • the user's device matches the offer's os (iOS offers on iOS, Android offers on Android);
  • the user isn't on a VPN or proxy.

If a check fails the user sees a message page instead (for example "Offer Not Available – This offer is not available in your country" or "VPN Detected"). Clicking the same offer again from the same device and IP reuses the open click instead of creating a new one.

Postbacks

When a user completes an offer (or a conversion is reversed) we call your postback URL so your server can add or remove the user's reward. Set it under Apps → Edit → Postback URL and put the macros you need in the query string; we replace them with real values.

Postback URL example
https://yourdomain.com/postback.php?user_id={user_id}&reward={reward}&payout={payout}&status={status}&offer={offerName}&country={country}&ip={user_ip}&debug={debug}&sig={sig}
Macros
MacroDescriptionExample
{user_id}Your user's ID, exactly as you put it in the offerwall link or API call.user42
{reward}Amount of your currency to give the user (payout × points rate).500
{payout}Your earning in USD for this conversion.0.5
{status}1 = add the reward, 2 = chargeback, subtract it.1
{offerName}Offer name. For multi-step offers the step is appended, e.g. Game-Level 10.FAB Mobile Banking - iOS
{country}User's country, ISO-2 code.US
{user_ip}User's IP address, when the network reports it.203.0.113.7
{debug}0 = live conversion, 1 = test sent from your dashboard.0
{sig}Signature to check the call is really from us (below).9e107d9d372bb682…

All values are URL-encoded. Fixed values in your URL (anything that isn't a macro) may only contain letters, numbers, _, {, } and commas, otherwise the URL is refused when you save it.

Signature
sig
sig = md5(app_id . user_id . postback_password)

Compute the same value on your server and reject the call if it doesn't match. The signature covers the App ID and user ID only, not the amounts, so keep your postback URL private.

Chargebacks

When an advertiser reverses a conversion we send status=2, and {reward} and {payout} are negative (for example -500). The same amount is taken off your balance. A conversion can only be charged back once.

Your response
  • We send one GET request per conversion or chargeback.
  • Reply with HTTP 200. Any other status code is recorded as failed.
  • We wait up to 10 seconds for your reply.
  • Failed postbacks are not retried automatically. Your balance is still credited; check Postback reports and reward the user by hand if needed.

Postback receiver samples

Both samples check the signature, add or subtract the reward and answer 200.

PHP
postback.php
<?php
// postback.php?user_id={user_id}&reward={reward}&status={status}&offer={offerName}&debug={debug}&sig={sig}

$appId          = '12345';             // your App ID
$postbackSecret = 'YOUR_POSTBACK_PASSWORD';

$userId = $_GET['user_id'] ?? '';
$reward = (float) ($_GET['reward'] ?? 0);   // negative on chargebacks
$status = $_GET['status'] ?? '';
$offer  = $_GET['offer'] ?? '';
$sig    = $_GET['sig'] ?? '';

$expected = md5($appId . $userId . $postbackSecret);

if ($userId === '' || !hash_equals($expected, $sig)) {
    http_response_code(403);
    exit('invalid signature');
}

if ($status === '1') {
    // add_points($userId, $reward, $offer);
} elseif ($status === '2') {
    // remove_points($userId, abs($reward), $offer);
}

http_response_code(200);
echo 'OK';
Node.js (no dependencies)
postback.js
// node postback.js  →  http://yourserver:3000/postback?user_id={user_id}&reward={reward}&status={status}&sig={sig}
const http = require('http');
const crypto = require('crypto');

const APP_ID = '12345';
const POSTBACK_SECRET = 'YOUR_POSTBACK_PASSWORD';

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost');
  if (url.pathname !== '/postback') { res.writeHead(404); return res.end(); }

  const q = url.searchParams;
  const userId = q.get('user_id') || '';
  const reward = parseFloat(q.get('reward') || '0'); // negative on chargebacks
  const status = q.get('status');
  const sig = q.get('sig') || '';

  const expected = crypto.createHash('md5').update(APP_ID + userId + POSTBACK_SECRET).digest('hex');
  const valid = userId !== '' && sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));

  if (!valid) { res.writeHead(403); return res.end('invalid signature'); }

  if (status === '1') {
    // addPoints(userId, reward);
  } else if (status === '2') {
    // removePoints(userId, Math.abs(reward));
  }

  res.writeHead(200);
  res.end('OK');
}).listen(3000);

Testing & reports

Test a postback

Open Apps → Edit → Offerwall & Postback Test. Enter a user ID and a payout, pick Valid or Chargeback, and press Test Postback. We call your saved postback URL with debug=1 and show your server's response code and body. The same tab also shows a live preview of your offerwall.

Test calls don't fill in {sig} and send the amounts as positive numbers, also for a chargeback test. A receiver that checks the signature will reject them, which is expected.
Postback reports

Postback Reports in the dashboard lists every postback we sent (live and test): the final URL, the result (success / failed), your server's response code and response body. Use it to find failed deliveries.

Payments & invoices

Balance

Each approved conversion adds its payout to your balance; each chargeback takes it off again. Every change is listed under Transactions.

Invoices
  • Payment terms: NET 30. An invoice is created every 30 days for your whole balance, which then goes back to 0.
  • Minimum payout: $10. If your balance is lower, it carries over to the next cycle.
  • Invoices are listed under Withdrawals with the amount, any deductions and the total paid. Status: Processing → Completed, or Rejected with a note from our team.
Payment method

Add your payout details under Payment Details before your first invoice. Available methods: USDT BEP20, Binance.

VPN & fraud rules

  • VPN and proxy traffic is blocked. Users on a VPN or proxy see "There's no offer available for you at this time." on the offerwall, get VPN Detected from the API, and see a "VPN Detected" warning if they click an offer.
  • Offers are only shown to and only open for users in the offer's countries and on the offer's device type.
  • Fraudulent or invalid conversions are reversed by the advertiser. You get a chargeback postback (status=2) and the amount is taken off your balance.
  • Our team can reject or disable an app, or disable an account, that sends invalid traffic.

Tip: only credit users from postbacks with a valid sig, and remove the reward again on status=2.

FAQ

The offerwall shows no offers. Why? Offers depend on the user's country and device, so some users see fewer offers. Check that the app is Active, the user isn't on a VPN or proxy, and, for the API, that you pass the user's IP and not your server's.
The link shows {"error":"App Pending"}. Your app hasn't been approved yet. Only your first app is active automatically; later apps are reviewed by our team.
My postbacks show as failed. Your server must answer HTTP 200 within 10 seconds. Open Postback Reports to see the response code and body we got. Failed postbacks aren't retried, but your balance is still credited.
How do I check that a postback is genuine? Compare sig with md5(app_id . user_id . postback_password); see Signature.
I generated a new Postback Password or API Key and things stopped working. The old value stops working as soon as the new one is generated. Update your server with the new value.
Can I change my points rate? Yes, under Apps → Edit. It applies to offers shown and conversions credited after you save.
When do I get paid? Every 30 days (NET 30) once your balance is at least $10, to the method in Payment Details.
How do I contact support? Email support@adspiritmedia.com with your App ID.