API Response Structure

The shapes the JSON API returns, and how to tell success from failure reliably.

The envelope

Every /api/v1 response has the same top-level structure:

{
  "success": true,
  "data": [ ... ],
  "meta": { ... }
}

Check the HTTP status first, then success. Note that /api/track does not return JSON — it issues a redirect.

Offers response

{
  "success": true,
  "data": [
    {
      "offer_id": 123,
      "name": "Complete a sign-up",
      "description": "Sign up for our newsletter",
      "payout": 1.2500,
      "payout_local": 1.14,
      "currency": "EUR",
      "category": "apps",
      "link": "https://preads.hansaldev.com/api/track?offerId=123&apiKey=...&userId=user_8812",
      "image_url": "https://.../logo.png",
      "countries": ["US", "CA"],
      "platforms": ["web"],
      "steps": []
    }
  ],
  "meta": {
    "total": 142,
    "returned": 20,
    "limit": 20,
    "offset": 0,
    "detected": {
      "country": "US",
      "device": "desktop",
      "os": "Windows"
    },
    "timestamp": 1759000000,
    "request_id": "req_68f2a1b0c3d4e"
  }
}
Field Meaning
offer_id Pass back as offerId to /api/track.
payout Your earnings from this offer, in USD.
payout_local The end user's reward, converted into your app's currency at your app's exchange rate. This is the figure to show to your user.
currency Your app's currency code, matching payout_local.
link A ready-made tracking URL for this offer, already carrying your API key and the user ID. Send the user here.
countries, platforms Where the offer is available. Useful for explaining why an offer may not convert.
steps Sub-tasks for multi-step offers. Empty for single-step offers.
meta.detected The country and device Preads detected for the caller. This is what filtering defaults to when you do not pass country or platform — so an empty result set is often a geo mismatch rather than an empty catalogue.
meta.total Total matching offers before pagination, not the size of data.
meta.request_id Quote this when reporting a problem — it identifies the exact request in the server logs.
payout and payout_local are not two views of one number. The first is what you earn; the second is what your user receives. They differ by your exchange rate.

Pagination

Use limit (1–100, default 20) and offset (default 0). Iterate with offset += limit while offset < meta.total, reading meta.total rather than assuming a page size.

Error responses

Errors use the same envelope with success: false and an error object:

{
  "success": false,
  "error": {
    "code": "INVALID_API_KEY",
    "message": "The provided API key or secret is invalid"
  },
  "meta": {
    "timestamp": 1759000000,
    "request_id": "req_68f2a1b0c3d4e"
  }
}

Validation errors carry the field-level detail in message:

{
  "success": false,
  "error": {
    "code": "INVALID_PARAMETERS",
    "message": {
      "limit": ["The limit may not be greater than 100."],
      "sort": ["The selected sort is invalid."]
    }
  },
  "meta": { "timestamp": 1759000000, "request_id": "req_..." }
}
Status Code Cause What to do
401 INVALID_API_KEY Key or secret did not match, or the app is not approved. Check both credentials and the app's approval status. Do not retry unchanged.
422 INVALID_PARAMETERS A parameter failed validation. Fix the fields named in error.message.
429 — Rate limit exceeded. Back off and cache. Do not retry immediately.
404 — On /api/track: the offer is gone or inactive. Re-fetch the offer list.
403 — On /api/track and the wall endpoints: the app exists but is not approved, or fraud detection refused the click. Distinguish by the message: "Invalid API key or app not approved" versus "Access Denied: Suspicious Activity Detected".
302 — On the wall and tracking endpoints: the request failed validation — a missing or unknown apiKey, or a missing userId on a click. The browser is sent back to the home page rather than shown an error, so a typo presents as "the wall does not work". Check the credentials and the user ID.
The tracking endpoint does not return JSON. On failure it returns plain text with a 403 or 404, or a 302 redirect when validation fails. Parse defensively, and read the body to tell a configuration problem from a fraud refusal.

Related documentation