# AffiliateOS advertiser integration: reference for AI coding assistants

> Snapshot, 30 October 2026. Written for www.affiliateos.ai from the AffiliateOS API as it runs today. Human-readable version: https://www.affiliateos.ai/help/developers#advertiser-events. Step-by-step guide: https://www.affiliateos.ai/help/getting-started/advertiser-first-campaign

Give this file to your coding assistant before you ask it to integrate AffiliateOS. Use only the endpoints and fields listed here. Ask the developer for the values marked "from the portal"; never invent them.

## What the integration does

1. When a player taps your campaign on a publisher's app or site, AffiliateOS records the tap and sends the player to your destination with `afos_click_id` added.
2. Your app or site stores that click ID on the player.
3. When the player completes a step you pay for, **your server** sends AffiliateOS an event with the click ID.
4. AffiliateOS matches the event to the tap, charges your balance the event's price and pays the publisher.

No SDK. Everything is server to server.

Each event has a dollar price you set in the portal. Advertisers can also pay a percent of each purchase or sale: our team sets that up with them. Publishers choose what they run, so an offer that doesn't let them make money won't get traffic.

## Values from the portal

| Value | Where |
|---|---|
| `appId` | Your product (app or site) in the advertiser portal. |
| `campaignId` | Optional. The campaign, under **Campaigns**. |
| Event keys | Each event you pay for has a key you chose, for example `chapter_1`, `level_25`, `starter_pack_purchase`. |
| API key | **Mint API key** on the campaign. Starts with `ak_`. Shown once. Server only. |

## Capture afos_click_id

- **Website campaigns:** read `afos_click_id` from the landing page URL, keep it (cookie or session), and attach it to the user at sign-up.
- **Android apps:** AffiliateOS puts the click ID in the Play Store `referrer`. Read it once on first launch with the Play Install Referrer API; the referrer string contains `afos_click_id=<id>`. Send it to your server and store it.
- **iOS apps, or any app without changes:** when a new player first opens the app, your server calls install matching and stores the click ID it returns:

```http
POST https://affiliateos.xyz/v1/advertiser/match-install
Authorization: Bearer ak_your_key
Content-Type: application/json

{ "appId": "your_app_id", "platform": "ios", "ip": "<the device's public IP>", "userAgent": "<optional>" }
```

Response: `{ "clickId": "...", "clickedAt": "..." }`, or `{ "clickId": null }` for an install with no tap of ours (usually organic; don't retry in a loop). Each click can match one install only. Call it once per new player, soon after first open.

- **If you use an attribution tool** such as AppsFlyer or Adjust: set the campaign's store link to your attribution link; `afos_click_id` arrives as a custom parameter in the install data. Read it on your server and store it.

Keep the first click ID you receive for a player unless your product decides otherwise.

## Send an event

```http
POST https://affiliateos.xyz/v1/advertiser/events
Authorization: Bearer ak_your_key
Content-Type: application/json
```

`x-api-key: ak_your_key` also works.

```json
{
  "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
}
```

| Field | Required | Meaning |
|---|---|---|
| `advertiserUserId` | Yes | Your own ID for the player. |
| `eventKey` | Yes | The event's key from the portal. |
| `eventId` | Yes | Unique per event. Reuse the same value on retries. |
| `timestamp` | Yes | ISO time the event happened. Must be within 48 hours of now. |
| `appId` | Yes | Your product's ID. |
| `clickId` | For live events | The stored `afos_click_id`. (`attributionId` or `installId` are accepted instead.) |
| `campaignId` | No | The campaign, if you want to be explicit. |
| `transactionId` | Purchases | Your order ID. Required for purchase and deposit events. |
| `revenue`, `currency` | When the event asks for it | The purchase amount and its currency. |
| `platform` | No | For example `web` on website campaigns. |
| `metadata` | No | An object of your own. |
| `test` | No | `true` to verify without charging. |

Unknown fields get HTTP 400. Send only the fields above.

### Response

```json
{ "status": "approved", "logId": "evl_3Hc9Qm", "payoutUsd": 1.5 }
```

| `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). 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 |

Auth failures return HTTP 401. Malformed bodies return HTTP 400.

## Go-live order

1. Create the product and the campaign, and add each event with its key and price.
2. Mint the API key and store it in your server's environment (never in git, never in the app).
3. Send each required event with `"test": true` (or press **Send test** in the portal) until it shows **Verified**. Test events are free.
4. Add at least $50 to your balance (**Add funds**). Funds are non-refundable.
5. **Submit for approval**. The campaign moves from Setting up to In review, then Live once approved.
6. Switch your server to `"test": false` for live traffic.

## Rules

- Send events from your server only. A key in an app can be read by anyone.
- Send events as they happen. A timestamp more than 48 hours away from now is refused as `outside_window`.
- Use a stable `eventId` per event so a retry returns `duplicate` instead of charging twice.
- A live event without a click ID is logged and never charged. That is correct behavior, not an error to work around.
- Never send a made-up click ID; it will not match a tap.
