Send events server to server (S2S)
When a user does something you pay for, your server sends one POST to https:// with your ak_ key and the click ID you stored for that user. We match it to the tap, charge the event’s price if it’s live and answer with a status.
See also: Setup without an SDK
On this page
- The user’s stored
afos_click_id: see Capture afos_click_id on your site or app. - Your
appIdandcampaignId: open the campaign and choose Show developer setup.
Mint API key
Under Show developer setup, choose Mint API key, or use Mint new key on API keys. The key starts with
ak_and is shown once, so copy it into your server’s settings straight away. It goes in your server only: never in your app or a web page.Send the event
Send it as soon as it happens, from your server:
curl -X POST "https://affiliateos.xyz/v1/advertiser/events" \ -H "Authorization: Bearer $AFOS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "clickId": "<stored afos_click_id>", "advertiserUserId": "tp_7731", "eventKey": "level_25", "eventId": "level_25_tp_7731", "timestamp": "2026-10-30T19:06:00.000Z", "appId": "<your appId>", "campaignId": "<your campaignId>" }'Example: Tidewater Putt player
tp_7731reaches level 25 at 2:06 PM in Texas.x-api-key: ak_...works in place of theAuthorizationheader.Field Required What it is clickIdOn live events The afos_click_idyou stored.attributionIdis accepted as the same thing.advertiserUserIdYes Your own ID for the user. Each event is paid once per user. eventKeyYes The event’s key, or its name in your attribution tool. Case doesn’t matter. eventIdYes Your unique ID for this one occurrence. A retry sends the same one. timestampYes When it happened, in ISO 8601. It must be within 48 hours of when we receive it. appIdYes Your product’s ID campaignIdNo Recommended, so we don’t have to find the campaign from the appIdand keytransactionIdFor purchases and deposits Your order ID. Required when the key contains purchaseordeposit, and it stops the same order being charged twice.revenue,currencyNo What the user spent, for your return on ad spend reporting. It doesn’t change the price you pay. platformNo ios,androidorwebtestNo truechecks the event without charging itSend only these fields. A field we don’t know gets the whole request refused with HTTP 400.
Read the answer
A request we accept gets a 2xx response with a JSON body such as
{"status": "approved", "logId": ".... The", "payoutUsd": 1. 5} statusdecides what happened:statusWhat it means Charged? approvedAccepted. A live event matched its tap. Yes, its price. A test isn’t charged. duplicateWe already have this eventIdortransactionId, or this user already had this eventNo. Treat it as success. missing_attributionNo click ID, or one we never issued No invalid_eventNo campaign for this appIdhas this event keyNo outside_windowtimestampis more than 48 hours from now, or past the event’s attribution window if it has oneNo campaign_pausedThe campaign is paused No insufficient_balanceYour balance can’t cover the price No, and your live campaigns pause rejectedSomething else is wrong. Event logs gives the reason, such as a missing transactionIdor a click ID used for another user.No A bad or revoked key gets HTTP 401. A request with a missing or unknown field gets HTTP 400: fix it before you send it again. A burst above our rate limit also gets HTTP 400, with a rate-limit message: wait, then retry with the same
eventId.Retry safely
If a request times out, gets a 5xx or hits the rate limit, send it again with the same
eventId. A repeat of an event we already have comes back asduplicateand is never charged twice. Fix a 400 for a bad field or a 401 before you retry: sending it again unchanged gets the same answer.Check it worked
Open the campaign, then Event logs. Each event shows its time, key, user, status and price, with the reason under it when there is one.
Before your campaign goes live, send each event with
"test": trueinstead: Send test events without being charged. The full reference is in the developer docs.