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

Интеграция Shakes.pro с Keitaro: API и постбэк

Интеграция Shakes.pro с Keitaro: API-ключ, offerId, streamCode и landingUrl, рабочий api.php, глобальный постбэк и таблица статусов сети и трекера…

Интеграция Shakes.pro с Keitaro: API и постбэк

Shakes.pro принимает заявки по API на адрес https://shakes.pro/?r=/api/order/in&key=API_KEY: обычная POST-форма с кодом потока, ID оффера, адресом оригинального лендинга и данными клиента. ID клика Keitaro передаётся в sub1 и возвращается в глобальном постбэке макросом {sub1}, по нему трекер записывает конверсию.

Ниже готовый api.php, форма, постбэк и таблица статусов. Своей открытой документации API у Shakes нет, поэтому поля сверены с официальной инструкцией Shakes в VK, справкой WebJack и проверкой адреса API 1 октября 2026 года. Что не удалось подтвердить, помечено в тексте.

Shakes.pro в 2026 году

  • Сеть работает. shakes.pro открывается, на главной: нутра COD для Латинской Америки, Европы и Азии, офферы больше чем 70 рекламодателей, три программы лояльности (Shakes Store, Shakes Collectibles, Spirit Club). Регистрация и вход на сайте.
  • API отвечает. На GET адрес возвращает {"status":"error","code":400,"message":"Bad request type"}, на POST с неверным ключом {"status":"error","code":403,"message":"API key is invalid"}.
  • Выплаты и холд мы сравнивали с другими сетями в подборке CPA-сетей для новичков. Офферы Shakes собраны в каталоге офферов.

Мы лили на Shakes немного. Из опыта 2024 года: с выплатами всё ровно, без задержек, менеджеры адекватные, офферы неплохие.

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

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

Как выбрать трекер под эту схему, мы разбирали в статье Keitaro, Binom или AIO.

Где взять API-ключ, ID оффера и код потока

Список данных взят из инструкции Shakes по API 2019 года. Она старая, но названия полей совпадают со справкой WebJack и с кодом из прошлой редакции этой статьи.

  1. API-ключ: «Профиль», «Редактировать личные данные», «Личная информация».
  2. ID оффера: цифры в конце адреса страницы оффера.
  3. Код потока: создайте поток на оффер, код виден в разделе «Потоки». Без потока не работает статистика Shakes.
  4. Адрес оригинального лендинга (landingUrl): ссылка на лендинг именно этого оффера в Shakes. Он нужен для правильной статистики.
  5. Готовый order.php. В 2024 году в разделе потоков была вкладка «API отправки лидов». Если файла там нет, Shakes советует запросить его у личного менеджера.

Параметры API Shakes.pro

Запрос отправляется методом POST, тело обычной формой (application/x-www-form-urlencoded), ключ в параметре key адреса. Обязательные поля перечислены по справке WebJack для Shakes.pro.

ПолеОбязательноЧто передавать
streamCodeдакод потока
offerIdдаID оффера. В справке WebJack записан как offerid, в коде прошлой редакции статьи как offerId
landingUrlдаадрес оригинального лендинга оффера
countryCodeдадвухбуквенный код страны по ISO 3166
name, phoneдаимя и телефон
createdAtдадата в формате Y-m-d H:i:s
ip, userAgentдаIP и user agent клиента
referrer, commentнетисточник перехода, комментарий
sub1для постбэкаsubid из Keitaro, вернётся макросом {sub1}
sub2…sub4нетсвои метки
Таблица прокручивается вбок

Страна должна входить в список гео оффера, иначе заказ не примут. Успешным обработчик считает ответ с "status":"ok", так проверял ответ и скрипт из прошлой редакции статьи. С настоящим ключом успешный ответ мы не проверяли. Если ваш order.php из кабинета называет поля иначе, используйте его написание.

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

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

<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}">
  <!-- уйдут в sub2-sub4 -->
  <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>

Если вы берёте лендинг Shakes с её order.php, справка Keitaro описывает другой путь: добавить в форму <input type="hidden" name="sub1" value="{subid}" />, а в order.php заменить чтение $_GET['sub1'] на $_POST['sub1']. Для своего лендинга на внешнем хостинге допишите к его ссылке в трекере ?subid={subid} и выводите subid в форме так:

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

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

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

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

// ===== Настройки =====
const API_URL      = 'https://shakes.pro/?r=/api/order/in&key=';
const API_KEY      = 'API_KEY';     // Профиль: "Редактировать личные данные", "Личная информация"
const OFFER_ID     = 'OFFER_ID';    // ID оффера: цифры в конце адреса страницы оффера
const STREAM_CODE  = 'STREAM_CODE'; // код потока из раздела "Потоки"
const LANDING_URL  = 'LANDING_URL'; // адрес оригинального лендинга этого оффера в Shakes
const COUNTRY      = 'CR';          // двухбуквенный 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 = [
    'createdAt'   => date('Y-m-d H:i:s'),
    'offerId'     => OFFER_ID,
    'streamCode'  => STREAM_CODE,
    'landingUrl'  => LANDING_URL,
    'countryCode' => COUNTRY,
    'name'        => $name,
    'phone'       => $phone,
    'ip'          => client_ip(),
    'userAgent'   => user_agent(),
    'referrer'    => referer_url(),
    'sub1'        => $subid,                // вернётся в постбэке макросом {sub1}
    'sub2'        => post_macro('utm_source'),
    'sub3'        => post_macro('utm_campaign'),
    'sub4'        => post_macro('utm_content'),
];

$res    = http_post(API_URL . rawurlencode(API_KEY), http_build_query(compact_fields($lead)), [
    'Content-Type: application/x-www-form-urlencoded',
]);
$answer = json_decode($res['body'], true);
$ok     = is_array($answer) && ($answer['status'] ?? '') === 'ok';

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.

Что изменилось по сравнению со скриптом 2024 года:

  • Данные уходят обычной формой. Старый код передавал в curl массив, и запрос уходил как multipart/form-data, а WebJack описывает для Shakes application/x-www-form-urlencoded.
  • Адрес по HTTPS. В старом коде был http://.
  • Нет подмены данных из адреса. Старый скрипт брал имя, телефон и ID оффера и из POST, и из GET. Теперь ID оффера и код потока заданы в настройках, а данные клиента берутся только из формы.
  • Телефоны не хранятся открытым текстом. Вместо leads.txt скрипт хранит HMAC-хеши номеров 24 часа, а журнал data/leads-ГГГГ-ММ-ДД.log пишет маскированный номер и ответ Shakes.
  • Таймаут 3 секунды по требованию Keitaro к PHP локальных лендингов, IP клиента за Cloudflare из CF-Connecting-IP.

Код проверен против локальной заглушки API в восьми сценариях и против настоящего адреса Shakes с неверным ключом: ответ 403 скрипт обработал как ошибку. После успеха он ведёт на success.php, после ошибки на error.php. Готовые страницы: «Спасибо за заказ» и страница ошибки.

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

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

Постбэк Shakes.pro в Keitaro

Шаг 1. Шаблон в Keitaro

В Keitaro откройте «Партнерские сети», нажмите «Создать» и выберите шаблон Shakes.pro. Так он выглядел в 2024 году: параметры для оффера sub1={subid}, в заметке просьба отметить в Shakes «Генерация конверсии», «Подтверждение конверсии» и «Отклонение конверсии».

Настройка постбеков в Кейтаро
https://ВАШ_ДОМЕН/POSTBACK_KEY/postback?subid={sub1}&payout={cost}&status={status}&currency={currency}&lead_status=none&sale_status=confirm,approved&rejected_status=reject,decline

ВАШ_ДОМЕН и POSTBACK_KEY возьмите в «Настройки», «Postback URL». Шаблон дописывает ещё stream, source_id и offer для своих отчётов, на запись конверсии они не влияют.

Шаг 2. Глобальный постбэк в кабинете Shakes

  1. Найдите страницу «Глобальный постбек». В 2024 году это была вкладка в разделе потоков рядом с «Источники», «Парковка», «Лог постбеков» и «API отправки лидов». В справке PeerClick путь указан как «Профиль», «Настройки».
  2. Вставьте ссылку из Keitaro в поле «Постбек».
  3. Отметьте «Генерация конверсии», «Подтверждение конверсии», «Отклонение конверсии» и «Невалидные заявки».
  4. Нажмите «Сохранить».
Настройка постбеков в партнерской программе shakes

Глобальный постбэк работает для потоков, у которых нет своего, локального. Чтобы он заработал на старых потоках, на той же странице есть кнопка «Сбросить постбек»: она убирает локальные постбэки со всех потоков. Отправки видны на вкладке «Лог постбеков».

Статусы Shakes и Keitaro

Значения status в открытых источниках Shakes не описаны. Таблица собрана по шаблону Keitaro 2024 года и справке AlterCPA, где reject относится к трешу.

Событие в ShakesЗначение statusОбучение в ссылкеКонверсия в Keitaro
Генерация конверсииnone по шаблону Keitaro, не подтвержденоlead_status=noneLead
Подтверждение конверсииconfirm или approvedsale_status=confirm,approvedSale
Отклонение конверсииdeclinerejected_status=reject,declineRejected
Невалидная заявкаrejectrejected_status=reject,declineRejected
Таблица прокручивается вбок

После первого лида откройте в Keitaro лог конверсий: в колонке «Статус» трекер сохраняет исходное значение от сети. Если там другое слово или постбэк в логе помечен «Conversion type not found», допишите это значение в нужный параметр обучения.

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

  • «Bad request type» (400). Запрос ушёл методом GET. Нужен POST.
  • «API key is invalid» (403). Неверный ключ или он передан не в параметре key адреса.
  • Заказ принят, но нет статистики по потоку. Не передан streamCode или landingUrl не от этого оффера.
  • Страна не из гео оффера. Shakes требует, чтобы countryCode совпадал с гео оффера.
  • В Keitaro «Click for subid REPLACE not found». В sub1 не ушёл ID клика: в форме нет поля с {subid} или лендинг открыт не через трекер. Остальные ошибки разобраны в справке Keitaro.
  • Постбэк не приходит по старым потокам. У потока остался локальный постбэк. Сбросьте его кнопкой «Сбросить постбек».

Бонус из прошлой редакции: архив лендинга Erogan CR на простатит для Коста-Рики, который хорошо конвертил у нас в 2024 году. Внутри лежит старый api.php: замените его кодом из этой статьи. Активен ли оффер сейчас, уточните у менеджера.

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

Какой адрес API у Shakes.pro?

https://shakes.pro/?r=/api/order/in&key=API_KEY, метод POST, данные обычной формой.

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

В кабинете: «Профиль», «Редактировать личные данные», «Личная информация».

Что такое landingUrl и streamCode?

landingUrl это адрес оригинального лендинга оффера в Shakes, streamCode это код потока. Оба нужны, чтобы заказ попал в статистику нужного потока.

В какое поле Shakes передавать subid Keitaro?

В sub1. Его возвращает макрос {sub1} в постбэке, так же настроен шаблон Shakes.pro в Keitaro.

Почему в Keitaro нет конверсий от Shakes?

Проверьте лог постбеков в кабинете Shakes и лог постбэков в Keitaro. Чаще всего постбэк перекрыт локальным постбэком потока или в лиде нет sub1.

Редакция BoostClicks

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