Conversions
Methods for the conversion log - everything that came in via postbacks from affiliate networks and from the traffic itself (installs, offer opens). Every event is visible here as a separate row: with the payout, the visitor and the status of sending to Facebook. For what the columns and statuses mean in the panel, see the "Conversions" section.
| Method | Address | What it does |
|---|---|---|
| GET | /conversions | List conversions for a period |
| GET | /conversions/{id} | One conversion and all its visitor's visits |
| POST | /conversions/{id}/re-export | Send a conversion to Facebook again |
Replace YOUR_KEY and panel.example.com with your own.
List conversions
GET/conversions
Conversions of your PWAs for a period, newest first.
| Parameter | Required | What it filters |
|---|---|---|
date_from, date_to | yes | Period by the event's received date: YYYY-MM-DD, whole days in UTC, no longer than a month - as in the "Statistics" section |
event[] | no | Event type, several allowed: install, open_offer, registration, deposit, repeat_deposit, page_view |
export_status | no | Status of sending to Facebook: exported, failed, not_exported or open_offer |
pwa_id or pwa_ids[] | no | One or several PWAs |
split_id or split_ids[] | no | One or several splits - conversions from their flows |
visitor_id | no | Part of the visitor identifier |
page, per_page | no | Page and its size: 100 by default, 200 maximum |
curl -G "https://panel.example.com/api/conversions" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json" \
--data-urlencode "date_from=2026-10-01" \
--data-urlencode "date_to=2026-10-05" \
--data-urlencode "event[]=deposit" \
--data-urlencode "event[]=repeat_deposit"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$page = 1;
do {
$response = $api->get('conversions', ['query' => [
'date_from' => '2026-10-01',
'date_to' => '2026-10-05',
'event' => ['deposit', 'repeat_deposit'],
'per_page' => 200,
'page' => $page,
]]);
$data = json_decode($response->getBody(), true)['data'];
foreach ($data['conversions'] as $conversion) {
echo "{$conversion['received_at']} {$conversion['pwa']['name']}: {$conversion['payout']} {$conversion['currency']}\n";
}
$page++;
} while ($data['meta']['current_page'] < $data['meta']['last_page']);# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
page = 1
while True:
response = requests.get(f"{API}/conversions", headers=HEADERS, params={
"date_from": "2026-10-01",
"date_to": "2026-10-05",
"event[]": ["deposit", "repeat_deposit"],
"per_page": 200,
"page": page,
}, timeout=30)
data = response.json()["data"]
for conversion in data["conversions"]:
print(conversion["received_at"], conversion["pwa"]["name"], conversion["payout"], conversion["currency"])
if data["meta"]["current_page"] >= data["meta"]["last_page"]:
break
page += 1// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
for (let page = 1; ; page++) {
const params = new URLSearchParams({
date_from: '2026-10-01',
date_to: '2026-10-05',
per_page: 200,
page,
});
params.append('event[]', 'deposit');
params.append('event[]', 'repeat_deposit');
const response = await fetch(`${API}/conversions?${params}`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();
for (const conversion of data.conversions) {
console.log(conversion.received_at, conversion.pwa.name, conversion.payout, conversion.currency);
}
if (data.meta.current_page >= data.meta.last_page) break;
}Response
{
"data": {
"meta": { "current_page": 1, "per_page": 200, "last_page": 1, "total": 1 },
"conversions": [
{
"id": "Rt5K",
"pwa": { "id": "WZAX", "name": "Promo EN", "icon_url": null },
"pwa_visitor_id": "7f3c2a9e-1b4d-4c8e-9a2f-5d6e7f8a9b0c",
"event": "deposit",
"raw_event": "ftd",
"payout": "45.00",
"currency": "USD",
"tid": "tx-55102",
"external_click_id": null,
"export_status": "failed",
"export_error": "[400] Malformed access token",
"exported_at": null,
"can_reexport": true,
"status": "scheduled",
"received_at": "05 Oct 2026 14:23",
"created_at": "05 Oct 2026 14:23",
"updated_at": "05 Oct 2026 14:23"
}
]
}
}| Field | What it is |
|---|---|
pwa | The PWA the conversion is credited to; null if the PWA couldn't be determined (only admin sees such rows) |
pwa_visitor_id | The visitor through whom the conversion was linked to a visit |
event | Event type (the list is in the parameters table above). null - the network sent a status the panel didn't recognise: such an event isn't counted in statistics and isn't sent to Facebook |
raw_event | The status exactly as the network sent it (ftd, sale, reg…) |
payout, currency | Payout from the postback - as a string, as it came |
tid | Transaction identifier from the postback |
external_click_id | click_id from the postback, if the network passed it |
export_status | Sending to Facebook - see below |
export_error | Facebook's answer if sending failed |
exported_at | When the event went to Facebook |
can_reexport | Whether it can be sent again: not for open_offer and events with event: null |
received_at | When the event arrived in the panel - the period filters by it |
status | A service field, irrelevant for scripts |
export_status:
exported- Facebook accepted the event.not_exported- the event hasn't been sent yet. Sending runs once a minute, but the event staysnot_exportedif the PWA has no pixel with a token, the event has no Facebook event assigned,eventisnullor the event is older than 3 days.failed- Facebook refused, the reason is inexport_error. Ifexported_atis empty, the error is temporary (Facebook unavailable, a limit) - the panel retries on its own. Ifexported_atis set, the refusal is final, a re-export is needed after fixing the cause.open_offer- a service event, it is never sent to Facebook.
Total for a period
To calculate revenue, add up payout for each currency separately - currencies don't combine into one number. Ready totals by currency for a period are in the "Statistics" section (total_deposits.amounts).
The export_status=open_offer filter shows only offer opens, the event[] parameter is ignored with it.
Errors
| Code | message | When |
|---|---|---|
422 | The date from field is required. (and 1 more error) | No period passed; the other period errors are as in the "Statistics" section |
422 | The selected event.0 is invalid. | Unknown event type in event[] |
422 | The selected export status is invalid. | Unknown export_status |
422 | The per page field must not be greater than 200. | per_page is greater than 200 |
422 | Invalid pwa_id / Invalid split_id | The PWA or split identifier isn't recognised |
404 | Not found | A PWA or split from the filter belongs to someone else or doesn't exist |
One conversion
GET/conversions/{id}
A conversion and up to 100 latest visits of its visitor: which domain and IP they came from, with which tags, and which visit brought the conversion. Handy when a network disputes a conversion or you need to find the creative that brought it.
curl https://panel.example.com/api/conversions/Rt5K \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$details = json_decode($api->get('conversions/Rt5K')->getBody(), true)['data'];
foreach ($details['visits'] as $visit) {
if ($visit['matched']) {
echo "The conversion came from the visit at {$visit['created_at']}, sub_1 = {$visit['subs']['sub_1']}\n";
}
}# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
details = requests.get(f"{API}/conversions/Rt5K", headers=HEADERS, timeout=30).json()["data"]
for visit in details["visits"]:
if visit["matched"]:
print("The conversion came from the visit at", visit["created_at"], "sub_1 =", (visit["subs"] or {}).get("sub_1"))// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const response = await fetch(`${API}/conversions/Rt5K`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();
for (const visit of data.visits) {
if (visit.matched) {
console.log('The conversion came from the visit at', visit.created_at, 'sub_1 =', visit.subs?.sub_1);
}
}Response
{
"data": {
"conversion": {
"id": "Rt5K",
"pwa": { "id": "WZAX", "name": "Promo EN", "icon_url": null },
"event": "deposit",
"payout": "45.00",
"currency": "USD",
"export_status": "failed",
"received_at": "05 Oct 2026 14:23"
},
"visits_count": 1,
"visits": [
{
"created_at": "05 Oct 2026 13:20",
"ip_address": "203.0.113.7",
"from_domain": "fit-tracker.example",
"country": "US",
"user_agent": "Mozilla/5.0 (Linux; Android 14)",
"offer_visitor_id": "aff-91822",
"offer_redirect_url": "https://offer.example/?sub1=fb-ua-creo1",
"subs": { "sub_1": "fb-ua-creo1", "sub_2": "adset-7" },
"third_party_tokens": null,
"is_unique": true,
"matched": true
}
]
}
}conversion is the same record as in the list (shortened here). visits_count - how many visits the visitor has in total; visits holds no more than the 100 latest, newest first.
| Visit field | What it is |
|---|---|
from_domain | The domain the visitor came to |
offer_visitor_id, offer_redirect_url | The visitor's identifier at the offer and the link they were sent to |
subs | Filled tags sub_1 … sub_20; null if there were no tags |
third_party_tokens | Other parameters from the link: fbclid, gclid and so on |
is_unique | This visitor's first visit |
matched | The visit that brought the conversion: the last visit to the same flow before the event arrived |
If no visit is marked matched, the linked visit wasn't found - most often because of a lost pwa_visitor_id in the postback.
Errors
| Code | message | When |
|---|---|---|
404 | Not found | The conversion belongs to someone else or doesn't exist |
Re-export
POST/conversions/{id}/re-export
Clears the sent mark and the error, and the next background sending (once a minute) tries to pass the event to Facebook again.
Useful when Facebook refused because of a setting you've already fixed: for example, you updated the pixel token. Fix the cause first - otherwise the retry gets the same error.
curl -X POST https://panel.example.com/api/conversions/Rt5K/re-export \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$response = $api->post('conversions/Rt5K/re-export');
$conversion = json_decode($response->getBody(), true)['data']['conversion'];
echo "{$conversion['id']}: {$conversion['export_status']}" . PHP_EOL;# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
response = requests.post(f"{API}/conversions/Rt5K/re-export", headers=HEADERS, timeout=30)
conversion = response.json()["data"]["conversion"]
print(conversion["id"], conversion["export_status"])// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const response = await fetch(`${API}/conversions/Rt5K/re-export`, {
method: 'POST',
headers,
signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();
console.log(data.conversion.id, data.conversion.export_status);The response is the conversion after the reset, its export_status is now not_exported:
Response
{
"data": {
"conversion": {
"id": "Rt5K",
"event": "deposit",
"export_status": "not_exported",
"export_error": null,
"exported_at": null,
"can_reexport": true
}
}
}Errors
| Code | message | When |
|---|---|---|
422 | Event cannot be re-exported | can_reexport: false - an offer open or an event with event: null |
404 | Not found | The conversion belongs to someone else or doesn't exist |
Only fresh conversions
Background sending takes events no older than 3 days since they were received (received_at). Re-exporting an older conversion resets its status, but it won't go to Facebook anymore.
To resend all errors at once, take the list with export_status=failed and call re-export for each row - no faster than the request limit from "API introduction".