Developer docs
Fetch offers, send events, verify every postback.
Request and response samples for publishers, advertisers and networks, checked against the API on , plus Markdown files your coding assistant can read.
curl -G "https://affiliateos.xyz/publisher/offers" \
-d country=US \
-H "x-api-key: pk_your_key" {
"id": "aff_7c2e91b04d3a",
"name": "Harbor Blocks",
"platforms": ["ios"],
"isPaidOnInstall": true,
"effectivePayoutUsd": 4.2,
"goals": [
{ "name": "Install",
"effectivePayoutUsd": 4.2 },
{ "name": "Reach level 10",
"effectivePayoutUsd": 0,
"userRewardUsd": 1.5 }
]
} - Publisher · Offer API Show offers in your app
- 1 Generate a key under My Profile, then API Keys
- 2 Call
GET /publisher/offerswithx-api-key - 3 Send users through
trackingUrlwith their ID assub_id
- Publisher · Offerwall Open our wall in your app
- 1 Copy the wall snippet from Docs in the portal
- 2 Fill in your user's ID as
uid, and their country - 3 Credit
user_payoutfrom each postback
- Advertiser Send events for a campaign
- 1 Mint API key on your campaign
- 2 POST each event with
"test": trueuntil Verified - 3 Send the stored
afos_click_idasclickIdon live events
- Network Pull offers for your publishers
- 1 Generate a key under Workspace settings, then API Keys
- 2
GET /network-api/offers, one page at a time - 3 Add your postback URL and a Webhook Secret
Two hosts, three keys, two secrets
Hosts and keys
https://affiliateos.xyz Wallhttps://offerwall.affiliateos.xyz/w/{appId} | Key | Starts with | Sent as | Where you get it |
|---|---|---|---|
| Publisher API key | pk_ | x-api-key header | My Profile, then API Keys, then Generate Key. Shown once. Up to five at a time. |
| Network API key | ak_ | x-api-key header | Workspace settings, then API Keys, then Generate Key. Shown once. Each key expires on the date in the key list, 90 days by default. The list shows how many keys you can hold: at one, ask us to raise it so the next key is live before the old one expires. |
| Advertiser API key | ak_ | Authorization: Bearer | Mint API key on your campaign. Shown once. Server only. |
| Postback secret | ps_ | Signs every postback | Publishers: Postbacks, then Create postback secret. Networks: Workspace settings, then Generate Webhook Secret. Shown once. |
| Offerwall app secret | ows_ | Signs wall launches | Only for signed launches. Keep it on your server. |
GET /publisher/offers
Offer API
Every offer your account can run, with your payout for each step and a tracking link ready for your user's ID. You decide the order and the screen.
Publisher key in the x-api-key header.
curl -G "https://affiliateos.xyz/publisher/offers" \
-d country=US -d platform=iOS -d page=1 \
--data-urlencode "name=harbor blocks" \
-H "x-api-key: $AFFILIATEOS_KEY" const url = new URL(
"https://affiliateos.xyz/publisher/offers",
);
url.search = new URLSearchParams({
country: "US",
platform: "iOS",
page: "1",
name: "harbor blocks",
});
const res = await fetch(url, {
headers: { "x-api-key": process.env.AFFILIATEOS_KEY },
});
const { data } = await res.json();
console.log(data.offers.length, data.pagination.total); import os, requests
r = requests.get(
"https://affiliateos.xyz/publisher/offers",
params={"country": "US", "platform": "iOS",
"page": 1, "name": "harbor blocks"},
headers={"x-api-key": os.environ["AFFILIATEOS_KEY"]},
)
offers = r.json()["data"]["offers"] {
"status": "success",
"data": {
"offers": [
{
"id": "aff_7c2e91b04d3a",
"name": "Harbor Blocks",
"trackingType": "CPI",
"platforms": ["ios"],
"isPaidOnInstall": true,
"effectivePayoutUsd": 4.2,
"trackingUrl":
"https://click.affiliateos.ai/click?off_id=…",
"goals": [
{
"id": "1",
"name": "Install",
"effectivePayoutUsd": 4.2,
"userRewardUsd": 1.5,
"userRewardIsAdvertiserSet": true
},
{
"id": "6",
"name": "Reach level 10",
"effectivePayoutUsd": 0,
"userRewardUsd": 1.5,
"userRewardIsAdvertiserSet": true
}
]
}
],
"pagination": {
"current_page": 1,
"per_page": 50,
"total": 1,
"has_more_pages": false
}
}
} Things that trip people up
- Paging is off unless you send
pageorper_page. Without them, one response holds every offer. - Put one ID per user in
sub_idon the tracking link. It comes back on the postback and decides whose balance you credit. - Keys are shown once. Keep up to five, so you can deploy a new key before you revoke the old one.
networkEpcis an estimate of earnings per click, never a guarantee.- When the same app is listed more than once, you get at most one install-paid and one event-paid listing per app and platform.
- Cards you saved earlier:
GET /publisher/offers/availability?ids=aff_...,aff_...checks 1 to 100 offer IDs. Each row returnsid,available,capandcapResetsAt, pluscheckedAton the response. Check at most every 60 seconds for cards on screen, and drop any card withavailable: false.
| Parameter | Example | What it does |
|---|---|---|
country | US | Only offers open to players in that country. Send it every time. |
state | TX | A US player's state. Leaves out offers whose state rules we enforce when they exclude it. |
platform | iOS | Only offers for that platform. |
os_version | 17.4 | With one platform: drops offers the phone is too old or too new for. |
segment | top | The Top Offers Feed, in our order. See the next section. |
page, per_page | 1, 100 | Turns paging on. per_page defaults to 50 and stops at 200. |
sort, sort_dir | payout, desc | Also epc, install_payout, first_event_payout, newest, popular. |
GET /publisher/offers?segment=top
Top Offers Feed
The same call, with the list chosen by us: our team's picks, plus offers that pay $1+, converted 10+ times in the last 30 days and had 15% or fewer reversed. Rebuilt every day.
Publisher key in the x-api-key header.
curl -G "https://affiliateos.xyz/publisher/offers" \
-d segment=top -d country=US \
-H "x-api-key: $AFFILIATEOS_KEY" const url = new URL(
"https://affiliateos.xyz/publisher/offers",
);
url.search = new URLSearchParams({
segment: "top",
country: "US",
});
const res = await fetch(url, {
headers: { "x-api-key": process.env.AFFILIATEOS_KEY },
});
// Rows arrive in our order: keep it.
const { data } = await res.json(); Things that trip people up
- Rows come back in a fixed order: install-paid games by highest install payout, then event-paid offers with our picks first.
sortis ignored. - Each row is your own row: your payout, your tracking link, your filters.
- The order is the same for every publisher. It is not personalized.
- If the Top Offers Feed is switched off,
segment=topreturns an empty list rather than an error.
GET offerwall.affiliateos.xyz/w/{appId}
Hosted Offerwall
A complete offerwall we host. Open it with your user's ID in a link, an iframe or a webview, and conversions arrive on your usual postback.
Simple launch: the uid in the URL. Signed launch: an HMAC signature made on your server, required once the wall is set to Signed.
<iframe
src="https://offerwall.affiliateos.xyz/w/owapp_your_app
?uid=u_48213&source=iframe&country=US"
style="width:100%;height:720px;border:0"
allow="clipboard-write"
></iframe> const crypto = require("crypto");
// Runs on your server. The ows_ secret stays there.
function wallUrl(uid, country) {
const appId = process.env.OFFERWALL_APP_ID; // owapp_
const secret = process.env.OFFERWALL_APP_SECRET; // ows_
const ts = Math.floor(Date.now() / 1000);
const rand = crypto.randomBytes(12).toString("hex");
const nonce = "nonce_" + rand;
const sig = crypto
.createHmac("sha256", secret)
.update([appId, uid, ts, nonce].join("\n"))
.digest("hex");
const base = "https://offerwall.affiliateos.xyz/w/";
const url = new URL(base + appId);
const params = { uid, ts, nonce, sig, country };
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, String(v));
}
return url.toString();
} Things that trip people up
- New walls start on Simple launches: the
uidalone opens the wall. Older walls may require a signature on every launch. We change this setting for you: email hello@affiliateos.ai with your wall ID (owapp_...), and switch your links over first. - A signed URL works once and for 5 minutes. Sign a fresh one per launch; a reused one shows “This offerwall link is invalid or expired.”
- Always send
country. The wall only filters by location when you do. - Credit
user_payoutfrom each postback as sent. Don't recompute it from the payout. On wall postbacks, your earnings are inrevenue. - Inside an iframe or webview the wall posts messages prefixed
affiliateos.offerwall.:ready,height_changed,offer_started,offer_redirect(iframes only),close_requested,error. Never credit from these: conversions reach you as postbacks. - On accounts with Properties & Apps, Test launch on the property's Integrations tab opens your wall in test mode, before you write signing code.
GET your URL
Postbacks
When a user completes a step, we send a GET to your URL with the conversion as query parameters, signed with your secret. Credit on completed, deduct on reversed.
We sign; you verify. verifier is HMAC-SHA256 of the full URL without verifier, keyed with your ps_ secret.
GET https://api.pebble.example/afos
?action_id=act_5Kq2Wc
&sub_id=u_48213
&offer_id=aff_7c2e91b04d3a
&status=completed
&payout=4.20
&user_payout=1.50
&conversion_name=Install
&offer_name=Harbor+Blocks
&goal_id=1
&is_offer_complete=false
&clicktimestamp=2026-10-29T19:06:00Z
&verifier=9f86d081884c7d659a2feaa0c55ad015… const crypto = require("crypto");
// Verify each postback before you credit anything.
app.get("/afos", (req, res) => {
const host = `${req.protocol}://${req.get("host")}`;
const url = new URL(host + req.originalUrl);
const verifier = url.searchParams.get("verifier");
url.searchParams.delete("verifier");
const secret = process.env.AFOS_POSTBACK_SECRET; // ps_
const expected = crypto
.createHmac("sha256", secret)
.update(url.toString())
.digest("hex");
if (expected !== verifier) {
return res.status(401).send("Invalid verifier");
}
// Credit on completed, deduct on reversed.
// Dedupe on action_id and status together.
res.sendStatus(200);
}); import hmac, hashlib, os
def is_from_affiliateos(full_url: str) -> bool:
# Strip verifier from the URL exactly as received.
# Never re-encode the rest.
base, _, query = full_url.partition("?")
kept, received = [], ""
for pair in query.split("&"):
if pair.startswith("verifier="):
received = pair[len("verifier="):]
else:
kept.append(pair)
rest = "&".join(kept)
unsigned = f"{base}?{rest}" if rest else base
secret = os.environ["AFOS_POSTBACK_SECRET"].encode()
expected = hmac.new(
secret, unsigned.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received) Things that trip people up
- Publisher postbacks are never sent unsigned. Without a postback secret, nothing is sent, even for real conversions.
- A reversal repeats the
action_idwithstatus=reversed. Dedupe onaction_idandstatustogether, or you drop the reversal. - Send test postback on a saved postback adds
test=true. It proves your URL answers, not that live conversions route there. - Install-paid games pay you once, at install. Later steps still post back with
user_payoutset to the player's reward:payout=0on Offer API, feed and link postbacks; on the Hosted Offerwall,revenue=0andpayoutequals the player's reward. - Verify against the URL exactly as you received it. Rebuilding it from a list of parameters breaks when an optional one appears.
- Every parameter, including
placement,manual_creditand the_numericIDs, is in Postback parameters. - Once you save any postback row, the account-level fallback URL stops being used. If you add event rows, add a conversion row too.
| Parameter | Sent | Meaning |
|---|---|---|
action_id | Always | The conversion's ID. A reversal repeats it. |
sub_id | Always | The user ID you put on the tracking link, or the wall's uid. |
offer_id | Always | Our offer ID, as in the Offer API. |
status | Always | completed or reversed. |
payout | Always | Offer API, feed and links: what we pay you for this step, in USD, 0 on later steps of install-paid games. Hosted Offerwall: the player's reward. |
user_payout | With a reward | The player's reward in USD. Credit this number as sent. |
revenue | Wall | Your earnings on Hosted Offerwall postbacks. Track revenue from it when present. |
conversion_name | Always | The step's name, such as Install or Reach level 10. |
offer_name | Usually | The offer's title. Store it: a pulled offer can't be looked up later. |
clicktimestamp | Usually | Click time as the advertiser's side reported it. The format varies; parse it defensively. |
goal_id | Per step | Matches goals[].id in the Offer API. |
is_offer_complete | Always | true when the last step is done. |
sub1 to sub10 | When the link had them | Your own values from the tracking link, returned on every postback for that conversion. |
reason | Some reversals | Why it was taken back, in plain words, when we have one. |
test | Tests only | true on a test postback. Don't credit it. |
verifier | Publishers: always | The signature. Strip it, hash the rest of the URL, compare. |
POST /v1/advertiser/events
Advertiser events
Tell us from your server when a player completes a step you pay for. We match it to the tap, charge your balance the event's price and pay the publisher.
Advertiser key as Authorization: Bearer ak_... (x-api-key also works). Server only.
curl -X POST \
"https://affiliateos.xyz/v1/advertiser/events" \
-H "Authorization: Bearer $AFOS_ADVERTISER_KEY" \
-H "Content-Type: application/json" \
-d '{
"clickId": "clk_7Q2f81",
"advertiserUserId": "tp_player_7731",
"eventKey": "level_25",
"eventId": "level_25_tp_player_7731",
"timestamp": "2026-10-29T19:31:00.000Z",
"appId": "your_app_id",
"test": true
}' const API = "https://affiliateos.xyz";
const key = process.env.AFOS_ADVERTISER_KEY;
await fetch(`${API}/v1/advertiser/events`, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
// stored from afos_click_id when the player arrived
clickId: user.afosClickId,
advertiserUserId: user.id,
eventKey: "level_25",
// stable, so a retry comes back as duplicate
eventId: `level_25_${user.id}`,
timestamp: new Date().toISOString(),
appId: process.env.AFOS_APP_ID,
test: false,
}),
}); {
"status": "approved",
"logId": "evl_3Hc9Qm",
"payoutUsd": 1.5
} Things that trip people up
- Test events (
"test": true) are free. Send one for each required event until it reads Verified. - Store
afos_click_idwhen the player arrives, and send it asclickIdon every live event. Without it the event is logged and never charged. - Use a stable
eventIdper event, so a retry comes back asduplicateinstead of a second charge. Purchases also needtransactionId. - Unknown fields in the body get a 400. Send only the fields listed here.
- Send events from your server. An API key in an app can be read by anyone.
| status | What happened | Charged |
|---|---|---|
approved | Matched and accepted. On a test event, verified and logged. | Live: the event's price. Test: no. |
duplicate | Same eventId or transactionId as before, or the player already reached this event's limit (such as once per player). Safe to treat as success. | No |
missing_attribution | No click ID came with a live event, or the one sent matches no tap of ours. | No |
invalid_event | No event with that eventKey on that appId. | No |
outside_window | timestamp isn't within 48 hours of now, or is later than the event's attribution window after the tap. | No |
campaign_paused | The campaign is paused. | No |
insufficient_balance | Your balance can't cover the event. Your live campaigns pause until you add funds. | No |
rejected | Something else failed. The reason is in Event logs on the campaign. | No |
GET /network-api/offers
Network API
Every offer your network can run, for your own walls and your publishers, in the same offer shape as the Offer API. Paged by default, 50 per page.
Network key in the x-api-key header. Your network's ak_ key, not an advertiser key.
curl -G "https://affiliateos.xyz/network-api/offers" \
-d country=US -d platform=iOS -d page=1 -d per_page=50 \
-H "x-api-key: $NORTHFOLD_NETWORK_KEY" const base = "https://affiliateos.xyz/network-api/offers";
const key = process.env.NORTHFOLD_NETWORK_KEY;
const all = [];
let page = 1;
for (;;) {
const qs = `?country=US&page=${page}&per_page=100`;
const res = await fetch(base + qs, {
headers: { "x-api-key": key },
});
const { data } = await res.json();
all.push(...data.offers);
if (!data.pagination.has_more_pages) break;
page += 1;
} Things that trip people up
/publisher-api/offersis the same route under its older name. Both answer.pub_idin a tracking link names your network. Leave it as we send it.- Network keys and advertiser keys both start with
ak_. Keep them apart: a network key reads offers, an advertiser key sends events. - Network keys expire, 90 days after you generate them by default. The key list shows each date and how many keys you can hold. To switch without a gap you need room for two: if your limit is one, ask us to raise it, then generate the next key before the old one expires.
- Generate a Webhook Secret under Workspace settings (it starts with
ps_) and every postback carries averifieryou can check, exactly as above.
Guides
Step by step, for the parts of an integration that need more than a sample.
- Reference API overview, hosts and authentication Which host each call goes to, which key each role uses and how to send it, and what a missing or wrong key returns. One page before any integration.
- How-to Give your coding assistant our API reference Paste the Markdown reference for your role into your coding assistant before you ask for code, so it builds on our real endpoints and fields.
- Reference Errors and rate limits What each HTTP error from our API means, the exact messages for key problems, what to fix before retrying and how we retry the postbacks we send you.
AI context files
Files for your coding assistant.
One plain Markdown file per role: endpoints, fields and rules, checked against the API on . Paste one in before you ask for code.
- Publisher
/ai/publisher.md
Publisher reference
Keys, the Offer API and Top Offers Feed, tracking links, postbacks and signatures, the Hosted Offerwall.
- Advertiser
/ai/advertiser.md
Advertiser reference
Products, campaigns and events,
afos_click_id, server events, test events and every response status. - Network
/ai/network.md
Network reference
The Network API, paging, tracking links for your publishers, postbacks and verifying them.
Then ask it this
Read the attached AffiliateOS reference before writing any code. Use only the endpoints and fields it lists, and ask me for keys instead of inventing them. Keep every key on the server.
AI tools can also start from the site index at /llms.txt, or read every help article as Markdown from /help/llms.txt.
Changelog
Latest changes for integrations
- Publishers
Set your own API user reward, as a percent of your payout, under My Profile. Where the advertiser set no reward, the Offer API's
userRewardUsdand your postbacks'user_payoutfollow it. Your payout doesn't change, and install steps keep a $0 user reward unless the advertiser set one. - Publishers
Add
sub1tosub10to a tracking link and get them back on that conversion's postbacks, as params or as{sub1}to{sub10}macros. - Publishers
The Postbacks screen lists 15 macros, up from 8. Two are new:
{offer_name}and{conversion_name}.
Get a key.
- Request publisher access
I run an app or site
Sign-up is by request. We reply by email.
- Start a campaign
I'm buying installs, sales or in-app events
Test events are free. Verify each required event before you go live.
- Book a demo
I run a network
From $500 a month.
Already have an account? Publisher log in or Advertiser log in. Networks: use the sign-in link from our team.