# Offers API

> List offers on your own pages, in your own design, instead of embedding the offerwall. Users start each offer from its start link, and rewards and postbacks work the same way.

Source: https://docs.moonetix.com/offers-api

## When to use it

The offerwall link is the quickest way to go live. Use the Offers API when you want to show offers inside your own site or app, for example next to offers from other networks, with your own cards and filters.

The offers you hide on the Offers page of your dashboard are left out of the API too, so it returns the same offers as your offerwall. See [Choose which offers your users see](https://docs.moonetix.com/offerwall-link#offer-filters).

- Your server asks for the offers of one user and gets them as JSON.
- You show them however you like.
- When the user picks one, you send them to its `start_url`. The offer page opens on Moonetix, and from there the user goes to the advertiser's app or site.
- Rewards arrive through your postback URL, exactly as with the offerwall. See [Postbacks](https://docs.moonetix.com/postbacks).

## Authentication

Every app has its own API key. Find it in your dashboard under Apps, your app, Integration, and send it in the `Authorization` header of every request:

```
Authorization: Bearer shk_your_api_key
```

Call the API from your server only. Never put the key in a browser, a mobile app, or a public repository. If it leaks, use New API key in the same tab: the old key stops working right away.

The API works for apps that are live, which a new app is right away. A paused app answers with `app_not_live`. The same key also works for the [Reporting API](https://docs.moonetix.com/reporting-api) and for [mobile SDK sessions](https://docs.moonetix.com/android-sdk#session).

## Request

`GET https://api.moonetix.com/v1/offers`

| Query parameter | Description |
| --- | --- |
| `user_id` (required, string) | The same user ID you use in offerwall links and receive in postbacks. 1 to 100 characters: letters, numbers, and `. _ @ : + -`. |
| `country` (required, string, ISO 3166-1 alpha-2) | The two-letter code of the country the user is in, for example `US`. Offers and rewards depend on it. |
| `device` (device or ua, string: desktop, android, ios) | The device of the user. |
| `ua` (device or ua, string) | The user agent of the user's browser. We read the device from it, so you can send it instead of `device`. |
| `sub1` to `sub5` (optional, string) | Your own tracking values. We add them to every `start_url` and return them in the postback as `{sub1}` to `{sub5}`. 1 to 100 characters: letters, numbers, and `. _ ~ : @ + -`. See [sub IDs](https://docs.moonetix.com/offerwall-link#sub-ids). |

| Header | Description |
| --- | --- |
| `Authorization` (required, string) | `Bearer`, a space, and your API key. |

The list is made for that user: offers they already completed, or cannot do again yet, are left out. Send the country the user really is in. When the user starts an offer, we check their country and device again, and an offer that does not match is not available to them.

## Examples

Pick your language once, and every example in the docs switches to it.

```bash
curl "https://api.moonetix.com/v1/offers?user_id=user_1234&country=US&device=android" \
  -H "Authorization: Bearer $MOONETIX_API_KEY"
```

```php
$query = http_build_query([
    'user_id' => $userId,
    'country' => $countryCode,
    'ua'      => $_SERVER['HTTP_USER_AGENT'] ?? '',
]);
$ch = curl_init('https://api.moonetix.com/v1/offers?' . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);
$data = json_decode(curl_exec($ch), true);
if (!empty($data['ok'])) {
    foreach ($data['offers'] as $offer) {
        // show $offer['title'], $offer['reward'], and a button to $offer['start_url']
    }
}
```

```javascript
const params = new URLSearchParams({
  user_id: userId,
  country: countryCode,
  ua: req.headers['user-agent'] || '',
});
const res = await fetch(`https://api.moonetix.com/v1/offers?${params}`, {
  headers: { Authorization: `Bearer ${process.env.MOONETIX_API_KEY}` },
});
const data = await res.json();
if (data.ok) {
  for (const offer of data.offers) {
    // show offer.title, offer.reward, and a button to offer.start_url
  }
}
```

```python
import os, requests

r = requests.get(
    "https://api.moonetix.com/v1/offers",
    params={"user_id": user_id, "country": country_code, "ua": user_agent},
    headers={"Authorization": f"Bearer {os.environ['MOONETIX_API_KEY']}"},
    timeout=10,
)
data = r.json()
if data.get("ok"):
    for offer in data["offers"]:
        pass  # show offer["title"], offer["reward"], and a button to offer["start_url"]
```

## Response

```json
{
  "ok": true,
  "currency": "Coins",
  "bonus_percent": null,
  "history_url": "https://wall.moonetix.com/APP_KEY?user_id=user_1234&hash=...#status",
  "offers": [
    {
      "id": 58,
      "title": "Nova Quest: reach level 10",
      "category": "game",
      "description": "Install the app, create a free account, and reach level 10.",
      "goals": [
        { "name": "Create an account", "reward": 325, "payout_usd": 0.5 },
        { "name": "Reach level 10", "reward": 650, "payout_usd": 1 }
      ],
      "reward": 975,
      "reward_before_bonus": null,
      "payout_usd": 1.5,
      "payout_type": "fixed",
      "reward_max": null,
      "payout_max_usd": null,
      "thumbnail_url": "https://moonetix.com/uploads/campaigns/thumb_1a2b3c.webp",
      "added_at": "2026-09-24T10:21:21+02:00",
      "start_url": "https://wall.moonetix.com/APP_KEY/offer/58?user_id=user_1234&hash=..."
    }
  ]
}
```

| Field | Description |
| --- | --- |
| `ok` (boolean) | `true` on success. Errors have `false`, see [Errors](https://docs.moonetix.com/offers-api#errors). |
| `currency` (string) | The name of your currency, as set in your app. |
| `bonus_percent` (number or null) | Your running bonus, if you have one. |
| `history_url` (string) | The user's My offers page on the offerwall. See [My offers and missing rewards](https://docs.moonetix.com/offers-api#history). |
| `offers` (array of objects) | The offers, see [Offer fields](https://docs.moonetix.com/offers-api#fields). It can be empty. |

## Offer fields

| Field | Description |
| --- | --- |
| `id` (integer) | The offer ID. The same value arrives as `{offer_id}` in postbacks. |
| `title` (string) | The name users see. It arrives as `{offer_name}` in postbacks. |
| `category` (string) | `website`, `app`, `game`, `service`, `store`, `social`, `community`, `signup`, `sweeps`, or `other`. |
| `description` (string) | What the user has to do to earn the reward, in the advertiser’s words, for example “Download and install this app, then run it for 30 seconds.” Show it with the offer so users know the task. The steps are in `goals`. Every offer is also for new users only, with no VPN and no emulator: say that once in your design, as the offerwall does. Plain text, not HTML, and it can contain line breaks. |
| `goals` (array of objects) | The steps in order, at least one, each with `name`, `reward`, and `payout_usd`. Every step is credited on its own and arrives as its own postback, with its own `{transaction_id}`. The `reward` and `payout_usd` of the offer are the totals of all steps. |
| `reward` (number) | What the user gets, in your currency, after your conversion rate, user split, bonus, and rounding. The same amount arrives as `{reward}` in the postback. |
| `reward_before_bonus` (number or null) | The reward without your bonus. It differs from `reward` only while your app runs a bonus, so you can show “was X”. `null` when no bonus is running. |
| `payout_usd` (number) | What you earn when the offer is credited, in US dollars. |
| `payout_type` (string) | `fixed` or `revshare`. A `revshare` offer has no fixed price: the advertiser pays a share of each deal the user completes, so one start can be credited several times, each as its own postback with its own `{transaction_id}`. For these offers `reward` and `payout_usd` are the lowest amount of one credit. |
| `reward_max` (number or null) | For `revshare` offers: the most one credit can reach, in your currency. Show it as “up to”. `null` for fixed offers. |
| `payout_max_usd` (number or null) | The same upper limit as `reward_max`, as your earnings in US dollars. `null` for fixed offers. |
| `thumbnail_url` (string) | The offer image. Offers without their own image get our default offer image, so this is never empty. |
| `added_at` (string, ISO 8601) | When the offer went live, in Budapest time with the offset. Use it for “New” badges or to sort by newest. |
| `start_url` (string) | The link that starts the offer for this user. Open it in a new tab or in your app's browser, and do not change it. It already carries the user ID and, if link security is on, the hash. |

New fields can be added over time, so ignore fields you do not know.

## Starting an offer

1. The user opens `start_url`. We check again that the offer is available for their country, device, and account.
2. The user opens the app or site from there and completes the steps. Each step is credited when the advertiser confirms it from their server.
3. The user sees the status on the page and in My offers on the offerwall. You get the result through your postback URL: status 1 when it is credited, and status 2 if it is reversed later.

Credit users only from postbacks, never when they click a start link.

## My offers and missing rewards

An offer started from the API is the same as one started on the offerwall. Its status, its credited steps, and a reversal all belong to the user ID and your app, not to where the offer was started.

- `history_url` opens the user's My offers page: every offer they started, with its status, rewards, and goals.
- If a reward does not arrive, the user can report it from My offers on the offerwall: each offer there has a Report button that opens [moonetix.com/missing-reward](https://moonetix.com/missing-reward) with the user ID and the offer filled in. Our team checks it with the advertiser.

Put a link to My offers somewhere users can find it, for example next to your offer list, so they can follow their offers. You get every result through your postback URL as usual.

## Errors

Errors come as JSON with `"ok": false`, a stable `error` code, and a `message` for your logs.

```json
{ "ok": false, "error": "invalid_country", "message": "country is required: the two-letter ISO code of the user, for example US." }
```

| HTTP | error | What to do |
| --- | --- | --- |
| 400 | `invalid_user_id`, `invalid_country`, `invalid_device` | Fix the parameter named in the code. |
| 401 | `invalid_api_key` | Send the key in the `Authorization` header, and copy it again if you replaced it. |
| 403 | `app_not_live` | The app is paused, or your account is not approved or not active. Check your notifications, or write to [partners@moonetix.com](mailto:partners@moonetix.com). |
| 403 | `user_blocked` | We blocked this user. Do not show them offers. |
| 404 | `not_found` | Check the address: `GET /v1/offers`. |
| 405 | `method_not_allowed` | Use `GET`. |
| 429 | `rate_limited` | Wait for the number of seconds in the `Retry-After` header. |

## Limits and caching

- Each app can make 600 requests per minute, Offers API and SDK session requests together. Above that, the API answers with `rate_limited` and a `Retry-After` header.
- Offers change as budgets run out and new offers start, so do not keep a list for long. Caching one user's list for 1 to 5 minutes is fine, and it keeps busy sites far below the limit. Do not share one user's list with others: it is filtered for them.
- An empty `offers` list is a normal answer. It means nothing is available for that user right now.

## OpenAPI and Postman

The Offers API, the [Reporting API](https://docs.moonetix.com/reporting-api), and the postback are also described in a machine-readable [openapi.json](https://docs.moonetix.com/openapi.json) file (OpenAPI 3.1). Tools read it so you do not have to copy anything by hand:

- In Postman or Insomnia, choose Import and paste `https://docs.moonetix.com/openapi.json`. You get a ready request: add your API key and send it.
- Code generators such as OpenAPI Generator can make a typed client for your language from the same file.

The file contains no keys or account data. It changes only when the API does.

## Quick reference

### Request

`GET api.moonetix.com/v1/offers`

```bash
curl "https://api.moonetix.com/v1/offers?user_id=user_1234&country=US&device=android" \
  -H "Authorization: Bearer $MOONETIX_API_KEY"
```

```php
$url = 'https://api.moonetix.com/v1/offers?' . http_build_query([
    'user_id' => $userId, 'country' => $country, 'device' => 'android',
]);
$ctx = stream_context_create(['http' => [
    'header' => 'Authorization: Bearer ' . $apiKey,
]]);
$data = json_decode(file_get_contents($url, false, $ctx), true);
```

```javascript
const params = new URLSearchParams({ user_id: userId, country, device: 'android' });
const res = await fetch(`https://api.moonetix.com/v1/offers?${params}`, {
  headers: { Authorization: `Bearer ${process.env.MOONETIX_API_KEY}` },
});
const data = await res.json();
```

```python
r = requests.get(
    "https://api.moonetix.com/v1/offers",
    params={"user_id": user_id, "country": country, "device": "android"},
    headers={"Authorization": f"Bearer {api_key}"},
)
data = r.json()
```

### Response 200

```json
{
  "ok": true,
  "offers": [
    {
      "id": 58,
      "title": "Nova Quest: reach level 10",
      "reward": 975,
      "payout_usd": 1.5,
      "start_url": "https://wall.moonetix.com/..."
    }
  ]
}
```

API key: Apps, your app, Integration. Up to 600 requests per minute, see [Limits](https://docs.moonetix.com/offers-api#limits).

Questions? [partners@moonetix.com](mailto:partners@moonetix.com) · [openapi.json](https://docs.moonetix.com/openapi.json)
