Setup without an SDK
Send events from your server. No SDK.
One server-to-server POST for each step a user completes. Test every event for free before you spend anything.
Test events are free. Verify each required event before you go live.
curl -X POST "$AFOS_API/v1/advertiser/events" \
-H "Authorization: Bearer $AFOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clickId": "clk_7Q2f81",
"advertiserUserId": "u_48213",
"eventId": "test-u_48213-level_10",
"timestamp": "2026-10-29T19:06:00Z",
"appId": "app_harbor_blocks",
"eventKey": "level_10",
"test": true
}' await fetch(`${process.env.AFOS_API}/v1/advertiser/events`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AFOS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
clickId: user.afosClickId, // saved on first open
advertiserUserId: "u_48213",
eventId: "test-u_48213-level_10", // same id on retries
timestamp: new Date().toISOString(),
appId: process.env.AFOS_APP_ID,
eventKey: "level_10",
test: true,
}),
}); import os, requests, datetime as dt
API_KEY = os.environ["AFOS_API_KEY"]
requests.post(
os.environ["AFOS_API"] + "/v1/advertiser/events",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"clickId": user.afos_click_id, # saved on first open
"advertiserUserId": "u_48213",
"eventId": "test-u_48213-level_10", # stable id
"timestamp": dt.datetime.now(dt.UTC).isoformat(),
"appId": os.environ["AFOS_APP_ID"],
"eventKey": "level_10",
"test": True,
},
timeout=10,
) { "status": "approved",
"payoutUsd": 5, "logId": "…" }
payoutUsd is the step's price, not a charge.
- Request received 2:06:00 PM
- API key checked 2:06:00 PM
- Request received, API key checked 2:06:00 PM
- Logged as a test event, $0.00 chargedLogged as a test, $0.00 charged 2:06:00 PM
- level_10 marked Verified 2:06:01 PM
- API key and host
- An
ak_key you mint in the campaign, kept on your server.AFOS_APIis the host in the event docs. - Click id
- Saved on the user when your app first opens or your page loads.
- Event keys
- One per step, such as
level_10. The dashboard suggests them.
01Responses
Every answer, and whether it charged.
Each request with a valid key gets one of these statuses back. Only a live approved event is
charged.
Retries are safe. Send the same eventId again and the answer is duplicate, never a
second charge. Give test events their own eventId: a live event that reuses a test's id also
comes back duplicate, and isn't charged.
| Status | What it means | Charged |
|---|---|---|
approved live | A live event with one of our clicks behind it, inside its window. Charged at your price for that step. | Yes |
approved test | A test event. Marks the step Verified in your dashboard. Tests are never charged. | No |
duplicate | The same eventId or transactionId arrived again, or this user already reached this step as many times as it pays (once by default). Safe to retry with the same id. | No |
missing_attribution | A live event with no click id we can find. Logged, never charged. | No |
outside_window | The event's timestamp is more than 48 hours from now, or the event falls outside that step's window, if it has one. | No |
campaign_paused | The campaign was paused when the event arrived. | No |
insufficient_balance | Your balance couldn't cover the step, so your live campaigns paused. Add funds, resume the campaign, then send the same event again within 48 hours of its timestamp and it can still charge. | No |
invalid_event | The appId, eventKey or campaign doesn't match one of yours. | No |
rejected | The campaign isn't live, the timestamp can't be read or the click belongs to another install or campaign. Also a purchase or deposit missing its transactionId or required revenue. | No |
A bad API key gets HTTP 401. A missing required field, or more than 120 requests a minute, gets HTTP
400. Neither is logged or charged.
02Install matching
Match the install three ways, none with an SDK.
Every paid event needs one of our click ids. These are the ways it reaches you. Pick the one that fits your app; you can use more than one.
-
01 Android
Play install referrer
Google Play hands our click id to your app in the install referrer on first open. Read it and save it on the user.
Exact -
02 iOS or Android
First-open match
When a new user first opens your app, your server calls us once with the device's IP address. We match the open to a recent click for the same app and platform from that address.
Confidence shown -
03 If you use one
Your attribution tool
When you add your app, enter your tool's tracking link as its App Store or Google Play URL. We add
No new trackingafos_click_idto it, so your tool can carry it into its install data. Forward the events your tool already sends your server with that click id, and map its event names to your event keys.
Websites: we add afos_click_id to your landing page URL. Save it when the page loads and send it back
with each event.
How a first-open match is decided
We check each recent click from that address against your app, the platform, the time since the click, whether another install already claimed it and whether our checks blocked it. If more than one person's click fits, we don't guess. Every decision shows on your Attribution page, with the checks each click passed and, on a match, its confidence.
Harbor Blocks: matched the only eligible click
| Click | App | Platform | Window | Unclaimed | Not blocked | Result |
|---|---|---|---|---|---|---|
| clk_7Q2f81 2:06 PM (8 min before) | App: passed | Platform: passed | Window: passed | Unclaimed: passed | Not blocked: passed | Eligible |
| clk_31b0c2 Yesterday, 9:40 PM | App: passed | Platform: passed | Window: failed | Unclaimed: passed | Not blocked: passed | Too old |
03AI brief
Hand the setup to your coding assistant.
A plain-text brief with the rules your developer, or their AI assistant, needs to wire this up.
- Where the click id comes from on the web, on Google Play and on iOS
- The request, its fields and the API host
- Safe retries, the 48-hour rule, purchases and when to turn
testoff
Inside a campaign, the dashboard has its own AI playbook with your app and campaign IDs filled in.
Your first event can be a test.
Mint an API key in your campaign and send the request above with test set to true.
Test events are free. Verify each required event before you go live.