Начало работы
- Владелец клуба выпускает ключ в портале: Настройки → API → «Выпустить ключ». Ключ показывается один раз, сохраните его.
- Выберите права: только чтение или чтение и запись. Для интеграции, которая только забирает данные, хватит чтения.
- Отправляйте запросы на
https://api.gym-one.ru/public/v1с заголовкомAuthorization: Bearer <ключ>.
curl https://api.gym-one.ru/public/v1/visitors?limit=10 \
-H "Authorization: Bearer $G1_API_KEY"
Персональные данные
Через API передаются персональные данные посетителей и тренеров клуба: имя, телефон, эл. почта, дата рождения, пол, абонементы, посещения, записи. По Федеральному закону № 152-ФЗ «О персональных данных» оператор этих данных — клуб. Gym One обрабатывает их по поручению клуба (п. 2.4 лицензионного соглашения). Интегратор — тот, кому клуб передал ключ, — тоже обрабатывает данные по поручению клуба (ч. 3 ст. 6 закона, п. 3.13 соглашения).
Что сделать клубу до передачи ключа интегратору:
- Заключить с интегратором договор с поручением на обработку персональных данных. По ч. 3 ст. 6 в нём определяются: перечень данных, действия с ними и цели обработки; обязанность интегратора соблюдать конфиденциальность и обеспечивать безопасность данных; требования ч. 5 ст. 18 (данные граждан России записываются и хранятся в базах данных на территории России) и ст. 18.1; обязанность по запросу клуба, в том числе до начала обработки, предоставлять документы о принятых мерах; требования к защите данных по ст. 19, в том числе обязанность уведомить клуб об утечке (случаи ч. 3.1 ст. 21).
- Получить согласие посетителей на поручение обработки интегратору, если оно требуется (ч. 3 ст. 6), и указать в согласии наименование или ФИО и адрес интегратора (п. 6 ч. 4 ст. 9). По запросу посетителя сообщать ему эти сведения (п. 9 ч. 7 ст. 14).
- Если интегратор — иностранное лицо или обрабатывает данные за пределами России, выполнить требования ст. 12 о трансграничной передаче.
Интегратору: обрабатывайте данные только в целях и объёме, указанных в договоре с клубом, не передавайте их третьим лицам без поручения клуба и храните ключ в тайне. Перед посетителем за действия интегратора отвечает клуб, интегратор отвечает перед клубом (ч. 5 ст. 6).
Когда договор с интегратором прекращён или ключ утёк, клуб отзывает ключ: запросы по нему сразу получают 401, неотправленные вебхуки отменяются. Gym One ведёт журнал запросов по каждому ключу (время, метод и адрес, код ответа, IP-адрес) и хранит его 90 дней. Последние 200 запросов клуб видит в портале: Настройки → API → «Журнал».
Авторизация
Каждый запрос, кроме openapi.json, передаёт ключ в заголовке:
Authorization: Bearer g1_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
| Ситуация | Ответ |
|---|---|
| Заголовка нет или ключ неверного формата | 401 |
| Ключ не найден или отозван | 401 |
| Запрос на запись (POST, PUT, DELETE) ключом «только чтение» | 403 |
| Клуб заблокирован | 403 |
Формат данных
- Тела запросов и ответов — JSON в UTF-8, заголовок
Content-Type: application/json. - Время — ISO 8601 со смещением часового пояса клуба:
2026-10-01T09:00:00+03:00. Даты —YYYY-MM-DD. - Телефон принимается в любом формате (
+7 900 123-45-67,89001234567), от 10 до 15 цифр. - Неизвестные поля в теле запроса не принимаются: ответ
400.
Ошибки
Ошибки приходят в формате RFC 9457 (application/problem+json). Причина по-русски — в поле detail, instance — номер запроса для обращения в поддержку.
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json; charset=utf-8
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"detail": "Нужен заголовок Authorization: Bearer <ключ API>",
"instance": "7bdb743e-2860-4cbc-bc30-f9bbc20488e9"
}
| Код | Значение |
|---|---|
200 201 204 | Успех; 201 — объект создан, 204 — без тела |
400 | Неверные параметры или тело запроса |
401 / 403 | Нет ключа / нет прав |
404 | Объект не найден (в том числе объект другого клуба) |
409 | Конфликт: нет мест, посетитель уже записан, у тренера перерыв, абонемент не позволяет вход |
422 | Связанный объект не найден или не подходит (посетитель, тип абонемента, занятие) |
429 | Превышен лимит запросов |
5xx | Сбой на нашей стороне, повторите запрос позже |
Лимиты
На каждый ключ: 600 запросов в минуту всего, из них не больше 120 на запись. В каждом ответе есть заголовки:
RateLimit-Limit: 600
RateLimit-Remaining: 587
RateLimit-Reset: 42
При превышении — ответ 429 и заголовок Retry-After с числом секунд до сброса.
Списки и постраничная выдача
Все списки принимают limit (1–200, по умолчанию 50), cursor и updated_since и отвечают так:
{
"data": [ { "id": 37, "name": "Иван Петров", … } ],
"next_cursor": "eyJ1IjoiMjAyNi0wOS0zMCAyMTozODowNCIsImkiOjM3fQ"
}
Чтобы получить следующую страницу, передайте next_cursor в параметре cursor. Когда next_cursor равен null, страниц больше нет.
updated_since (ISO 8601) возвращает только записи, изменённые начиная с этого момента. Так удобно синхронизировать изменения: запомните время последней выгрузки и в следующий раз запросите только новое.
Повтор запроса без дублей
Операции создания (POST) принимают заголовок Idempotency-Key: от 1 до 100 печатных ASCII-символов, например UUID. Повтор с тем же ключом в течение суток не создаст второй объект, а вернёт первый ответ с заголовком Idempotent-Replayed: true. Тот же ключ с другим телом запроса — ответ 422.
curl -X POST https://api.gym-one.ru/public/v1/visitors \
-H "Authorization: Bearer $G1_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0c2b8e-1d7a-4c1e-9a55-2f3b8c0d9e11" \
-d '{"name":"Иван Петров","phone":"+7 900 123-45-67"}'
Вебхуки: формат и подпись
Когда в клубе происходит событие, Gym One отправляет POST на адрес, заданный через PUT /webhook. Один вебхук на ключ, только https://.
| Событие | Когда | data |
|---|---|---|
visitor.created | Добавлен посетитель (в портале, в ЛК или через API) | посетитель |
subscription.purchased | Выдан абонемент | абонемент |
visit.created | Отмечено посещение | посещение |
workout.entered | Посетитель пришёл на групповую тренировку или тренировку по записи | посещение |
lead.stage_changed | Заявка перешла на другую стадию воронки: вручную, после покупки абонемента, после первого посещения, возврат после отказа. Только с модулем «Привлечение и продажи» | заявка и стадии |
data события lead.stage_changed: lead_id, name, phone, email, source (callback, vitrina, yandex, site, qr, import, api, referral, manual, phone, vk, vk_ads, avito, 2gis), source_label, utm_source, utm_medium, utm_campaign, funnel — название воронки, stage_from и stage_to — {id, title, kind}, где kind: open — обычная стадия, trial — пробное, won — купил, lost — отказ; reason — причина отказа или что произошло, visitor_id — карточка посетителя, если заявка с ней связана, responsible_user_id, changed_at, created_at. У обезличенной заявки (отказ старше 12 месяцев) имени и контактов нет; у заявки из сообщений ВКонтакте или Авито телефона и почты может не быть.
POST https://example.com/g1-hook
Content-Type: application/json
X-G1-Event: visitor.created
X-G1-Delivery: 0b9d7c1e-6c1f-4f7e-8a3b-1c2d3e4f5a6b
X-G1-Signature: t=1790793484,v1=5c1f…e9a2
{
"id": "0b9d7c1e-6c1f-4f7e-8a3b-1c2d3e4f5a6b",
"type": "visitor.created",
"created_at": "2026-09-30T21:38:04+03:00",
"data": { "id": 91, "name": "Иван Петров", "phone": "79001234567", … }
}
Ответьте кодом 2xx в течение 10 секунд. Иначе доставка повторится через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч и 24 ч. Если объект удалили до отправки, data будет null. Одно событие может прийти дважды — различайте события по id.
Проверка подписи. v1 — HMAC-SHA256 от строки t + "." + тело запроса на секрете вебхука (выдаётся один раз при первом PUT /webhook), в hex. Сверяйте подпись с сырым телом запроса, до разбора JSON, и отклоняйте запросы со старым t (например, старше 5 минут).
import crypto from 'node:crypto'
function verify (rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const mac = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1))
}
function verify(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int)$p['t']) > 300) return false;
$mac = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($mac, $p['v1']);
}
// $rawBody = file_get_contents('php://input');
// $header = $_SERVER['HTTP_X_G1_SIGNATURE'];
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
p = dict(x.split('=', 1) for x in header.split(','))
if abs(time.time() - int(p['t'])) > 300:
return False
mac = hmac.new(secret.encode(), p['t'].encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, p['v1'])