Перейти к содержанию

Интеграция Affstar с Keitaro: lead API и постбэк

Affstar lead API и Keitaro: где взять ApiKey и хеш потока, обязательные поля, рабочий api.php, настройка постбэка и соответствие статусов сети и трекера…

Интеграция Affstar с Keitaro: lead API и постбэк

Affstar принимает лиды через lead API: ваш лендинг отправляет POST-запрос в JSON на https://api.affstar.com/api/lead/create/publisher с ключом ApiKey и хешем потока. ID клика Keitaro уходит в поле sub_id1 и возвращается в постбэке, по нему трекер записывает конверсию и её статус.

Ниже по шагам: где взять ApiKey и хеш потока, какие поля обязательны, рабочий api.php, постбэк и таблица статусов. Поля и ответы API сверены с разделом «API» кабинета Affstar (его тексты загружаются вместе с кодом страницы my.affstar.com) и проверены запросом к API 1 октября 2026 года.

Affstar в 2026 году

  • Сеть работает. affstar.com открывается, описание на сайте: «CPA-сеть и рекламодатель нового поколения», белые и нутра-офферы, приём трафика в том числе из ВК и Facebook. Регистрация есть на сайте, кабинет вебмастера на my.affstar.com.
  • API на месте. Адрес приёма лидов прежний, на запрос с неверным ключом он отвечает {"success":false,"errors":["Unauthorized"]} и кодом 401.
  • В кабинете есть всё для интеграции: раздел «Инструменты» с пунктами «Постбэки», «Лог постбэков», «Парковка домена» и «API».

Наш опыт 2024 года: топовые офферы, разные гео, адекватные менеджеры и выплаты без задержек. Из минусов были низкие ставки на старте, долгие согласования гарантов и всего остального. Чем Affstar отличается от других сетей и с какой начинать, мы разбирали в подборке CPA-сетей для новичков.

Как работает связка Affstar и Keitaro

  1. Клик приходит в Keitaro, трекер создаёт subid и показывает ваш лендинг.
  2. В форме лендинга Keitaro подставляет subid вместо макроса {subid}.
  3. api.php отправляет лид в Affstar, subid уходит в sub_id1.
  4. Affstar присылает постбэк с {sub_id1} и статусом: новый, подтверждённый, отклонён или треш. Keitaro записывает конверсию.

Трекер под такую схему мы сравнивали в статье Keitaro, Binom или AIO.

Где взять ApiKey и хеш потока

  1. Попросите доступ к API. Affstar предупреждает: «Убедитесь, что для вашего аккаунта разрешен прием лидов по API. Свяжитесь со своим аффилейт-менеджером».
  2. ApiKey. Откройте профиль my.affstar.com/profile: кликните по аватару в шапке и выберите «Редактировать». Ключ лежит на вкладке безопасности, там же его можно показать, скопировать или перевыпустить. Если ключ перевыпустили, замените его и в api.php.
  3. Хеш потока. Создайте поток на оффер и скопируйте его хеш. Это UUID: на строку другого формата API ответит «This is not a valid UUID».

Affstar lead API: адрес, авторизация, поля

Запрос отправляется методом POST, данные в JSON. Ключ передаётся в заголовке ApiKey или параметром api_key в адресе. Максимальная длина subid и utm-меток 255 символов.

ПолеОбязательноЧто передавать
flow_uuidдахеш потока
ipдаIP посетителя, который оставил заявку
country_codeдадвухбуквенный ISO-код страны
phoneдателефон
nameдаимя
user_agentв описании среди обязательныхuser agent посетителя
sub_id1для постбэкаsubid из Keitaro
sub_id2, sub_id3нетсвои метки
utm_source, utm_medium, utm_campaign, utm_content, utm_termнетutm-метки
subaccount, referer, commentнетсубаккаунт, источник перехода, комментарий
Таблица прокручивается вбок

В русской версии раздела country_code описан как двухбуквенный ISO-код, в английской как числовой. Обработчик ниже шлёт две буквы, как в русской версии. Если API вернёт ошибку по этому полю, уточните формат у менеджера.

Ответы по описанию в кабинете:

  • лид создан: {"success":true,"data":{"lead_uuid":"0de94314-…"}};
  • ошибка в поле: {"success":false,"errors":[{"text":"This is not a valid UUID.","field":"flow_uuid"}]};
  • неверный ключ: {"success":false,"errors":["Unauthorized"]};
  • лид отклонён: невалидный номер или «The order has already been placed to this phone number», то есть заказ с этим телефоном уже в обработке.

Статус лида можно запросить и без постбэка: GET https://api.affstar.com/api/lead/get-status?lead_uuid=ID_ЛИДА с той же авторизацией. Для нескольких лидов есть /api/lead/get-statuses?lead_list_uuid[]=…. Статусы в ответе: new, approved, rejected, trash.

Форма на лендинге

Affstar советует для API-интеграции переименовать лендинг из html в php и убрать с него JS-скрипт сети. Главный файл назовите index.php, тогда Keitaro подставит макросы. Каждой форме поставьте method="post" и action="api.php" и добавьте скрытые поля:

<form action="api.php" method="post">
  <input type="text" name="name" placeholder="Имя" required minlength="2" maxlength="100">
  <input type="tel" name="phone" placeholder="Телефон" required>
  <!-- ID клика Keitaro: трекер подставит его вместо {subid} -->
  <input type="hidden" name="subid" value="{subid}">
  <input type="hidden" name="referrer" value="{referrer}">
  <!-- Affstar принимает utm-метки как есть, до 255 символов -->
  <input type="hidden" name="utm_source" value="{utm_source}">
  <input type="hidden" name="utm_campaign" value="{utm_campaign}">
  <input type="hidden" name="utm_content" value="{utm_content}">
  <button type="submit">Заказать</button>
</form>

Если лендинг на своём хостинге, а не в Keitaro, допишите к его ссылке в трекере ?subid={subid}, а в форме читайте значение из адреса с экранированием:

<!-- ссылка на лендинг в Keitaro: https://ВАШ_ЛЕНДИНГ/?subid={subid} -->
<input type="hidden" name="subid" value="<?= htmlspecialchars($_GET['subid'] ?? '', ENT_QUOTES) ?>">

PHP-обработчик api.php для Affstar

Заполните API_KEY, FLOW_UUID и COUNTRY. Нужен PHP 7.2 или новее с curl и mbstring.

<?php
// api.php: принимает заявку с формы лендинга и отправляет лид в Affstar по API
declare(strict_types=1);

// ===== Настройки =====
const API_URL      = 'https://api.affstar.com/api/lead/create/publisher';
const API_KEY      = 'API_KEY';     // my.affstar.com/profile: ApiKey (доступ к API включает менеджер)
const FLOW_UUID    = 'FLOW_UUID';   // хеш потока: создайте поток и скопируйте его
const COUNTRY      = 'ES';          // двухбуквенный ISO-код страны оффера
const SUCCESS_PAGE = 'success.php';
const ERROR_PAGE   = 'error.php';
const HTTP_TIMEOUT = 3;             // секунды: Keitaro ждёт от PHP локального лендинга не дольше 3 с
const DEDUP_TTL    = 86400;         // не отправлять тот же телефон повторно 24 часа
const DATA_DIR     = __DIR__ . '/data';

function go(string $page, array $query = []): void
{
    $query = array_filter($query, 'strlen');
    header('Location: ' . $page . ($query ? '?' . http_build_query($query) : ''), true, 303);
    exit;
}

function post_str(string $key, int $max = 255): string
{
    $v = isset($_POST[$key]) && is_string($_POST[$key]) ? trim(strip_tags($_POST[$key])) : '';
    return mb_substr($v, 0, $max);
}

function client_ip(): string
{
    // За Cloudflare в REMOTE_ADDR адрес узла Cloudflare, реальный IP посетителя в CF-Connecting-IP
    foreach (['HTTP_CF_CONNECTING_IP', 'REMOTE_ADDR'] as $k) {
        $ip = $_SERVER[$k] ?? '';
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
            return $ip;
        }
    }
    return '';
}

function landing_url(): string
{
    $https = ($_SERVER['HTTPS'] ?? '') === 'on' || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
    $host  = preg_replace('/[^A-Za-z0-9.:-]/', '', $_SERVER['HTTP_HOST'] ?? '');
    return ($https ? 'https' : 'http') . '://' . $host . '/';
}

function post_macro(string $key): string
{
    // Keitaro подставляет значения макросов в URL-кодировке: пробел придёт как "+"
    return mb_substr(trim(urldecode(post_str($key, 1000))), 0, 255);
}

function referer_url(): string
{
    // Откуда пришёл посетитель: поле referrer формы ({referrer} в Keitaro), иначе адрес лендинга
    foreach ([post_macro('referrer'), (string) ($_SERVER['HTTP_REFERER'] ?? '')] as $ref) {
        if (filter_var($ref, FILTER_VALIDATE_URL)) {
            return mb_substr($ref, 0, 500);
        }
    }
    return landing_url();
}

function user_agent(): string
{
    return mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255);
}

function compact_fields(array $fields): array
{
    return array_filter($fields, static function ($v): bool {
        return $v !== '';
    });
}

function data_file(string $name): string
{
    if (!is_dir(DATA_DIR) && @mkdir(DATA_DIR, 0750, true)) {
        @file_put_contents(DATA_DIR . '/.htaccess', "Require all denied\n");
    }
    return DATA_DIR . '/' . $name;
}

function log_event(array $row): void
{
    $line = json_encode(['time' => date('c')] + $row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
    @file_put_contents(data_file('leads-' . date('Y-m-d') . '.log'), $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}

function mask_phone(string $digits): string
{
    return substr($digits, 0, 3) . '***' . substr($digits, -2);
}

// Защита от дублей: в файле хранятся не номера, а их HMAC, чтобы файл не стал базой телефонов
function phone_seen(string $digits, bool $remember = false): bool
{
    $fp = @fopen(data_file('phones.json'), 'c+');
    if ($fp === false) {
        return false; // нет прав на запись: лид не блокируем
    }
    flock($fp, LOCK_EX);
    $list = json_decode((string) stream_get_contents($fp), true);
    $list = is_array($list) ? $list : [];
    $now  = time();
    foreach ($list as $k => $t) {
        if ($now - (int) $t > DEDUP_TTL) {
            unset($list[$k]);
        }
    }
    $key  = hash_hmac('sha256', $digits, API_KEY);
    $seen = isset($list[$key]);
    if ($remember) {
        $list[$key] = $now;
    }
    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, (string) json_encode($list));
    flock($fp, LOCK_UN);
    fclose($fp);
    return $seen;
}

function http_post(string $url, string $body, array $headers): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => HTTP_TIMEOUT,
        CURLOPT_TIMEOUT        => HTTP_TIMEOUT,
    ]);
    $answer = curl_exec($ch);
    return [
        'code'  => (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
        'body'  => is_string($answer) ? $answer : '',
        'error' => curl_error($ch),
    ];
}

// ===== Обработка заявки =====
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
    go('index.php');
}
if (!function_exists('curl_init')) {
    log_event(['result' => 'no_curl']);
    go(ERROR_PAGE);
}

$name   = post_str('name', 100);
$phone  = (string) preg_replace('/[^\d+]/', '', post_str('phone', 32));
$digits = (string) preg_replace('/\D/', '', $phone);
$subid  = post_str('subid', 100);
$subid  = preg_match('/^[A-Za-z0-9._-]+$/', $subid) ? $subid : ''; // {subid} не подставился = пусто
$thanks = ['name' => $name, 'phone' => $phone, 'pxl' => (string) preg_replace('/\D/', '', post_str('pxl', 32))];

if (mb_strlen($name) < 2 || strlen($digits) < 7 || strlen($digits) > 15) {
    go(ERROR_PAGE);
}
if (phone_seen($digits)) {
    log_event(['result' => 'duplicate', 'phone' => mask_phone($digits), 'subid' => $subid]);
    go(SUCCESS_PAGE, $thanks);
}

$lead = [
    'flow_uuid'    => FLOW_UUID,
    'country_code' => COUNTRY,
    'name'         => $name,
    'phone'        => $phone,
    'ip'           => client_ip(),
    'user_agent'   => user_agent(),
    'referer'      => referer_url(),
    'sub_id1'      => $subid,               // вернётся в постбэке макросом {sub_id1}
    'utm_source'   => post_macro('utm_source'),
    'utm_medium'   => post_macro('utm_medium'),
    'utm_campaign' => post_macro('utm_campaign'),
    'utm_content'  => post_macro('utm_content'),
    'utm_term'     => post_macro('utm_term'),
];

$json   = (string) json_encode(compact_fields($lead), JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
$res    = http_post(API_URL, $json, [
    'Content-Type: application/json',
    'ApiKey: ' . API_KEY,
]);
$answer = json_decode($res['body'], true);
$ok     = is_array($answer) && ($answer['success'] ?? false) === true;

log_event([
    'result' => $ok ? 'sent' : 'error',
    'phone'  => mask_phone($digits),
    'subid'  => $subid,
    'http'   => $res['code'],
    'curl'   => $res['error'],
    'answer' => mb_substr($res['body'], 0, 500),
]);

if (!$ok) {
    go(ERROR_PAGE);
}
phone_seen($digits, true);
go(SUCCESS_PAGE, $thanks);

Перед рабочим запуском. Проверка номера и запись после ответа API выполняются раздельно: одновременные запросы могут пройти оба. Для защиты от гонки нужна атомарная операция или поддерживаемый сетью ключ идемпотентности. Папку data держите вне публичного каталога: создаваемый .htaccess не защищает её в nginx. В журнал попадает ответ API, который может содержать персональные данные или секреты; маскируйте их до записи. Заголовку CF-Connecting-IP доверяйте только от проверенного прокси Cloudflare.

Что делает скрипт:

  • принимает только POST, проверяет имя и телефон (от 7 до 15 цифр);
  • отправляет лид в JSON с заголовком ApiKey, как в примере кода из кабинета Affstar;
  • считает лид принятым только при "success":true, при любой ошибке ведёт на error.php, а текст ошибки пишет в data/leads-ГГГГ-ММ-ДД.log;
  • не отправляет повторно тот же номер 24 часа и хранит для этого HMAC-хеши, а не сами номера. Почему дубли бьют по апруву, мы писали в статье про дубли лидов;
  • ждёт ответ не дольше 3 секунд: таково требование Keitaro к PHP локальных лендингов;
  • за Cloudflare берёт IP посетителя из заголовка CF-Connecting-IP, а не адрес узла Cloudflare.

Старый скрипт из прошлой редакции статьи вёл на страницу «спасибо» только при success, но хранил телефоны открытым текстом в leads.txt и брал IP только из REMOTE_ADDR. Новый код мы проверили против локальной заглушки API в восьми сценариях и против настоящего API Affstar с неверным ключом. Ответ 401 скрипт обработал как ошибку и записал в журнал. Страницы для редиректа: «Спасибо за заказ» и страница ошибки.

Как проверить отправку

  1. Загрузите архив с index.php, api.php, success.php и error.php в Keitaro как локальный оффер и добавьте его в тестовую кампанию без фильтров.
  2. Откройте ссылку кампании и проверьте в коде страницы, что в поле subid стоит ID клика.
  3. Отправьте форму. В журнале должна появиться строка "result":"sent" и ответ с lead_uuid.
  4. Найдите лид в «Детализации» кабинета Affstar: в колонке subid1 должен стоять ID клика.

Постбэк Affstar в Keitaro

Шаг 1. Партнёрская сеть в Keitaro

В Keitaro откройте «Партнерские сети» и нажмите «Создать». Если шаблона Affstar в списке нет, заполните поля вручную, как на скриншоте 2024 года: название Affstar, параметры для оффера sub_id1={subid} и Postback URL.

Настройка постбеков для ПП affstar и keitaro
https://ВАШ_ДОМЕН/POSTBACK_KEY/postback?subid={sub_id1}&payout={amount}&currency={currency}&status={status}&lead_status=new&sale_status=approved&rejected_status=rejected,trash

ВАШ_ДОМЕН и POSTBACK_KEY возьмите в «Настройки», «Postback URL». Макросы {sub_id1}, {amount}, {currency} и {status} взяты из шаблона 2024 года. В открытом описании API их нет, поэтому сверьте их со списком на странице создания постбэка в кабинете.

Шаг 2. Постбэк в кабинете Affstar

  1. Откройте «Инструменты», «Постбэки» и нажмите «Создать постбэк».
  2. Отметьте «Активный» и «Глобальный постбэк». Если глобальный не нужен, выберите потоки.
  3. Метод запроса: GET.
  4. Отметьте статусы «Новый», «Подтвержденный», «Отклонен» и «Трэш» и задайте им названия new, approved, rejected и trash.
  5. Вставьте ссылку из Keitaro в поле URL и нажмите «Сохранить».
Постбеки в партнерской сети Affstar.com и Кейтаро

В той же форме есть «Повторная отправка»: постбэк уйдёт ещё раз, если вознаграждение изменится после апрува. Ещё там есть опция «Отключить передачу постбэка второго звонка». Все отправки видны в «Инструменты», «Лог постбэков»: дата, ID лида, статус, код ответа трекера и URL. Если код ответа не 200, ищите причину в логе Keitaro.

Статусы Affstar и Keitaro

Статус в AffstarНазвание в постбэкеКонверсия в Keitaro
НовыйnewLead, доход в холде
ПодтвержденныйapprovedSale
ОтклоненrejectedRejected
ТрэшtrashRejected
Таблица прокручивается вбок

Названия статусов в Affstar можно задать любые. Если назвать их сразу lead, sale и rejected, обучение в ссылке Keitaro не понадобится. Чтобы видеть треш отдельной колонкой, в Keitaro есть тип конверсии Trash и параметр trash_status.

Частые ошибки

  • «Unauthorized». Неверный ApiKey, ключ перевыпустили или аккаунту не открыт приём лидов по API.
  • «This is not a valid UUID» в поле flow_uuid. Вместо хеша потока вставлен ID оффера или ссылка целиком.
  • Заказ с этим телефоном уже в обработке. Повторная заявка: Affstar её не примет. Проверьте, не отправляет ли форма лид дважды.
  • В Keitaro «Click for subid REPLACE not found». В sub_id1 не ушёл ID клика. Проверьте поле subid в форме и что лендинг открыт через трекер.
  • «Conversion type not found». Названия статусов в Affstar не совпадают с обучением в ссылке. Разбор кодов ошибок есть в справке Keitaro.
  • Тестовый постбэк не создал конверсию. В тестовом постбэке нет настоящего subid, проверяйте реальным лидом.

Бонус из старой редакции: архив лендинга Keto-Matcha Blue ES, который мы в 2024 году активно лили на Affstar. В архиве только HTML, обработчик из статьи нужно добавить самому. Жив ли оффер сейчас, уточните у менеджера.

Частые вопросы

Где взять API ключ Affstar?

В профиле my.affstar.com: аватар в шапке, «Редактировать», ApiKey. Приём лидов по API для аккаунта включает менеджер.

Какой адрес у Affstar lead API?

https://api.affstar.com/api/lead/create/publisher, метод POST, тело в JSON, ключ в заголовке ApiKey.

Какие поля обязательны в API Affstar?

flow_uuid, ip, country_code, phone и name. Для постбэка добавьте sub_id1 с ID клика.

Как узнать статус лида Affstar без постбэка?

GET-запросом на /api/lead/get-status?lead_uuid=… с тем же ключом. Ответ вернёт new, approved, rejected или trash.

Почему Affstar не отправляет постбэк в Keitaro?

Откройте «Лог постбэков» в кабинете. Если записей нет, постбэк не активен или не привязан к потоку, если есть код ошибки, смотрите лог постбэков в Keitaro.

Редакция BoostClicks

Материалы о настройке рекламы, трекинге и автоматизации: инструкции, примеры кода и расчёты с указанием условий проверки.