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

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 и Keitaro
- Где взять API-ключ, ID оффера и код потока
- Параметры API Shakes.pro
- Форма на лендинге
- PHP-обработчик api.php для Shakes.pro
- Как проверить отправку
- Постбэк Shakes.pro в Keitaro
- Шаг 1. Шаблон в Keitaro
- Шаг 2. Глобальный постбэк в кабинете Shakes
- Статусы Shakes и Keitaro
- Частые ошибки
- Частые вопросы
- Какой адрес API у Shakes.pro?
- Где взять API-ключ Shakes?
- Что такое landingUrl и streamCode?
- В какое поле Shakes передавать subid Keitaro?
- Почему в Keitaro нет конверсий от Shakes?
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
- Клик приходит в Keitaro, трекер создаёт subid и показывает лендинг.
- Keitaro подставляет subid в скрытое поле формы вместо
{subid}. api.phpотправляет заказ в Shakes и кладёт subid вsub1.- Shakes шлёт глобальный постбэк при генерации, подтверждении и отклонении конверсии и по невалидным заявкам. Keitaro находит клик по
{sub1}и записывает конверсию.
Как выбрать трекер под эту схему, мы разбирали в статье Keitaro, Binom или AIO.
Где взять API-ключ, ID оффера и код потока
Список данных взят из инструкции Shakes по API 2019 года. Она старая, но названия полей совпадают со справкой WebJack и с кодом из прошлой редакции этой статьи.
- API-ключ: «Профиль», «Редактировать личные данные», «Личная информация».
- ID оффера: цифры в конце адреса страницы оффера.
- Код потока: создайте поток на оффер, код виден в разделе «Потоки». Без потока не работает статистика Shakes.
- Адрес оригинального лендинга (
landingUrl): ссылка на лендинг именно этого оффера в Shakes. Он нужен для правильной статистики. - Готовый 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. Готовые страницы: «Спасибо за заказ» и страница ошибки.
Как проверить отправку
- Загрузите архив с
index.php,api.php,success.phpиerror.phpв Keitaro как локальный оффер и создайте тестовую кампанию с одним потоком. - Откройте ссылку кампании и проверьте в коде страницы поле
subid: там должен быть ID клика. - Отправьте форму. В журнале в папке
dataнужна строка"result":"sent". - Найдите заказ в статистике 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}¤cy={currency}&lead_status=none&sale_status=confirm,approved&rejected_status=reject,declineВАШ_ДОМЕН и POSTBACK_KEY возьмите в «Настройки», «Postback URL». Шаблон дописывает ещё stream, source_id и offer для своих отчётов, на запись конверсии они не влияют.
Шаг 2. Глобальный постбэк в кабинете Shakes
- Найдите страницу «Глобальный постбек». В 2024 году это была вкладка в разделе потоков рядом с «Источники», «Парковка», «Лог постбеков» и «API отправки лидов». В справке PeerClick путь указан как «Профиль», «Настройки».
- Вставьте ссылку из Keitaro в поле «Постбек».
- Отметьте «Генерация конверсии», «Подтверждение конверсии», «Отклонение конверсии» и «Невалидные заявки».
- Нажмите «Сохранить».

Глобальный постбэк работает для потоков, у которых нет своего, локального. Чтобы он заработал на старых потоках, на той же странице есть кнопка «Сбросить постбек»: она убирает локальные постбэки со всех потоков. Отправки видны на вкладке «Лог постбеков».
Статусы Shakes и Keitaro
Значения status в открытых источниках Shakes не описаны. Таблица собрана по шаблону Keitaro 2024 года и справке AlterCPA, где reject относится к трешу.
| Событие в Shakes | Значение status | Обучение в ссылке | Конверсия в Keitaro |
|---|---|---|---|
| Генерация конверсии | none по шаблону Keitaro, не подтверждено | lead_status=none | Lead |
| Подтверждение конверсии | confirm или approved | sale_status=confirm,approved | Sale |
| Отклонение конверсии | decline | rejected_status=reject,decline | Rejected |
| Невалидная заявка | reject | rejected_status=reject,decline | Rejected |
После первого лида откройте в 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.



