PaceTrack Partner API

Developers

RU|EN|ES

PaceTrack Partner API

Публичный API для сторонних приложений: OAuth-согласие атлета, чтение фактов и wellness, чтение и запись структурированных тренировок. Канонический hostname — pacetrack.ru. Английская версия этой страницы — канон.

Кабинет разработчика

Соглашения

  • База API: https://pacetrack.ru/api/partner/v1
  • JSON UTF-8, заголовок Content-Type: application/json.
  • Публичные id непрозрачные: атлет ptu_, план ptp_, факт ptw_, wellness ptwl_, приложение ptapp_, access pta_, refresh ptr_.
  • User-запросы: заголовок Authorization: Bearer pta_….
  • Webhook-эндпоинты приложения: HTTP Basic client_id:client_secret или те же поля в JSON/form.
  • CORS выключен: API сервер-сервер, не из браузера.
  • Запросы приложения выполняются от имени атлета. Блокировка дня, недели или учётной записи атлета возвращает 403 plan_locked.
  • Списки содержат только опубликованные планы. Приложение может получить собственный черновик по идентификатору, возвращённому при его создании.

Заголовки ответа (каждый запрос к API и /oauth/token)

X-RateLimit-Limit-Second: 10
X-RateLimit-Remaining-Second: 7
X-RateLimit-Limit-Hour: 1000
X-RateLimit-Remaining-Hour: 812
X-RateLimit-Limit-Day: 10000
X-RateLimit-Remaining-Day: 6401
X-UserLimit-Limit: 50
X-UserLimit-Remaining: 12
Retry-After: 12

Лимиты по умолчанию: 10 запросов в секунду, 1 000 в час и 10 000 в сутки. Для операций записи (POST, PUT, DELETE) действует лимит 20% от каждого значения. X-UserLimit содержит лимит подключённых атлетов, а не запросов. Заголовок Retry-After возвращается только с ответом 429. Лимиты могут быть настроены отдельно для приложения.

Пагинация списков: limit (планы/факты 1–200, дефолт 50; wellness 1–400, дефолт 50) и курсор after = id последнего элемента предыдущей страницы. Ответ содержит has_more. Глубокий offset не поддерживается.

OAuth

Используется Authorization Code Flow с PKCE S256. Поддерживаются только confidential-клиенты с client_secret. Срок действия access-токена — 1 час. Срок действия refresh-токена — 180 дней с момента последнего обновления. При обновлении сервер выдаёт новый refresh-токен и немедленно отзывает предыдущий.

GET https://pacetrack.ru/oauth/authorize

Браузерный экран согласия атлета. Успех — редирект на redirect_uri с code и state. Отказ — error в query.

ПараметрОбяз.Описание
client_idдаПубличный id приложения, ptapp_…
redirect_uriдаДолжен точно совпадать с URI из списка разрешённых. Требуется HTTPS; HTTP разрешён только для localhost у приложений с режимом разработки.
response_typeдаcode
scopeдаСписок через пробел. profile обязателен. plans:publish только вместе с plans:write.
stateдаЭхо на redirect. Проверяет партнёр.
code_challengeдаBASE64URL(SHA256(code_verifier))
code_challenge_methodдаS256
https://pacetrack.ru/oauth/authorize?client_id=ptapp_…&redirect_uri=https://example.com/cb&response_type=code&scope=profile%20plans:read%20plans:write&state=…&code_challenge=…&code_challenge_method=S256

POST https://pacetrack.ru/oauth/token

Тело: application/x-www-form-urlencoded или JSON. Клиент — HTTP Basic или поля client_id / client_secret.

grant_type=authorization_code

ПолеОписание
codeОдноразовый код с authorize
redirect_uriТот же URI, что в authorize
code_verifierСекрет PKCE, из которого считали challenge

grant_type=refresh_token

ПолеОписание
refresh_tokenТекущий ptr_…

Response 200

{
    "access_token": "pta_…",
    "refresh_token": "ptr_…",
    "expires_in": 3600,
    "scope": "profile plans:read plans:write",
    "token_type": "Bearer"
}

Scopes

ScopeЧто даёт
profileИмя, opaque id, локаль, виды спорта. Без email.
workouts:readКарточка завершённой тренировки и FIT-файл в пределах настроенного скользящего окна (по умолчанию 30 дней). GPS-трек и ряды метрик не входят.
workouts:trackGPS-трек и ряды метрик тренировки. Используется только вместе с workouts:read. Запрос GET …/track без этого scope возвращает 403 insufficient_scope.
workouts:writeСоздавать факты тренировок через POST /v1/workouts (JSON) или POST /v1/workouts/fit-uploads (FIT-файл, асинхронно). Не требует workouts:read.
plans:readОпубликованные планы и структура. Черновики в списке не отдаются.
plans:writeСоздавать и менять свои черновики.
plans:publishПубликовать черновик и менять/удалять опубликованный незалоченный день. Без этого → 403 publish_required.
wellness:readДневные метрики сна/готовности/нагрузки, то же окно 30 дней.
wellbeing:readДневник самочувствия атлета (данные о здоровье). Не входит в wellness:read.
offlineСистемный scope для выдачи refresh-токена. Сервер добавляет его к авторизации приложения автоматически.

GET/api/partner/v1/me

Профиль согласившегося атлета. Email не отдаётся.

Bearer · profile

Response

{
    "id": "ptu_…",
    "name": "Alex Runner",
    "locale": "ru",
    "sports": [
        "running",
        "cycling"
    ]
}

sports — коды из опубликованных планов, фактов и предпочтения календаря.

POST/api/partner/v1/connection/revoke

Отзывает подключение атлета к приложению. Все токены подключения становятся недействительными, а атлет перестаёт учитываться в лимите подключений.

Bearer · любой валидный токен

Response

{
    "ok": true
}

GET/api/partner/v1/plans

Опубликованные дни плана. Черновики не входят. Сортировка по умолчанию: plan_date ASC, id ASC.

Bearer · plans:read

QueryОписание
fromYYYY-MM-DD, нижняя граница plan_date
toYYYY-MM-DD, верхняя граница
limit1–200, дефолт 50
afterКурсор: ptp_… последнего элемента
updated_afterDATETIME. Меняет сортировку на updated_at ASC, id ASC.
created_afterDATETIME, фильтр по created_at

Response

{
    "plans": [
        {
            "id": "ptp_…",
            "date": "2026-08-25",
            "sport": "running",
            "title": "Tempo 8k",
            "description": "<p>Easy warmup, then tempo.</p>",
            "distance_km": 10,
            "duration_s": 3600,
            "is_draft": false,
            "is_locked": false,
            "structured_workout": null,
            "updated_at": "2026-08-25 12:01:03",
            "version": 3,
            "partner_app_id": "ptapp_…"
        }
    ],
    "has_more": false
}

POST/api/partner/v1/plans

Создаёт день всегда как черновик (is_draft: true). HTTP 201.

Bearer · plans:write

Body

ПолеТипОписание
dateYYYY-MM-DDДата дня. На POST обязательна (алиас plan_date).
sportstringКод вида из справочника: running, cycling, swimming, xc_ski (лыжные гонки) и другие. Дефолт running.
titlestringДо 255 символов.
descriptionHTMLQuill HTML, санитайзится. Пустое → заглушка абзаца.
distance_kmnumberПлановая дистанция.
duration_sintПлановая длительность, секунды.
structured_workoutobject|nullКанонический JSON структуры. На PUT null снимает структуру. PUT /structure принимает объект или обёртку {structured_workout}.
expected_versionintCAS, если нет заголовка If-Match. Timestamp / expected_updated_at → 400.

Response

{
    "id": "ptp_…",
    "date": "2026-08-25",
    "sport": "running",
    "title": "Tempo 8k",
    "description": "<p>Easy warmup, then tempo.</p>",
    "distance_km": 10,
    "duration_s": 3600,
    "is_draft": false,
    "is_locked": false,
    "structured_workout": null,
    "updated_at": "2026-08-25 12:01:03",
    "version": 3,
    "partner_app_id": "ptapp_…"
}

Идемпотентность: заголовок Idempotency-Key (или поле тела). Повтор с тем же ключом в окне 24 ч возвращает тот же план, без дубликата. Публикация только через POST …/publish — поле is_draft в теле игнорируется.

GET/api/partner/v1/plans/{id}

Один опубликованный день, либо свой черновик этого приложения по id из POST. Чужой черновик → 404.

Bearer · plans:read

Response

{
    "id": "ptp_…",
    "date": "2026-08-25",
    "sport": "running",
    "title": "Tempo 8k",
    "description": "<p>Easy warmup, then tempo.</p>",
    "distance_km": 10,
    "duration_s": 3600,
    "is_draft": false,
    "is_locked": false,
    "structured_workout": null,
    "updated_at": "2026-08-25 12:01:03",
    "version": 3,
    "partner_app_id": "ptapp_…"
}

version — целочисленный CAS. updated_at информационное, с точностью до секунды, не etag. partner_app_id — публичный id приложения-источника или null.

PUT/api/partner/v1/plans/{id}

Заменить карточку и/или структуру. Свой черновик — достаточно write. Опубликованный день — ещё plans:publish.

Bearer · plans:write

Body

ПолеТипОписание
dateYYYY-MM-DDДата дня. На POST обязательна (алиас plan_date).
sportstringКод вида из справочника: running, cycling, swimming, xc_ski (лыжные гонки) и другие. Дефолт running.
titlestringДо 255 символов.
descriptionHTMLQuill HTML, санитайзится. Пустое → заглушка абзаца.
distance_kmnumberПлановая дистанция.
duration_sintПлановая длительность, секунды.
structured_workoutobject|nullКанонический JSON структуры. На PUT null снимает структуру. PUT /structure принимает объект или обёртку {structured_workout}.
expected_versionintCAS, если нет заголовка If-Match. Timestamp / expected_updated_at → 400.

Response

{
    "id": "ptp_…",
    "date": "2026-08-25",
    "sport": "running",
    "title": "Tempo 8k",
    "description": "<p>Easy warmup, then tempo.</p>",
    "distance_km": 10,
    "duration_s": 3600,
    "is_draft": false,
    "is_locked": false,
    "structured_workout": null,
    "updated_at": "2026-08-25 12:01:03",
    "version": 3,
    "partner_app_id": "ptapp_…"
}

Оптимистичная блокировка: заголовок If-Match: <version> или тело expected_version. Расхождение → 409 (в теле текущие version и updated_at). Timestamp в If-Match → 400.

DELETE/api/partner/v1/plans/{id}

Удалить свой черновик. Опубликованный день — write + publish. Можно передать If-Match / expected_version.

Bearer · plans:write

Response

{
    "ok": true
}

PUT/api/partner/v1/plans/{id}/structure

Только структура. Тело — объект structured_workout или {"structured_workout": …}. Ошибки валидатора → 400 invalid_structure со списком errors.

Bearer · plans:write

Body

{
    "schema": 1,
    "sport": "running",
    "steps": [
        {
            "id": "wu-1",
            "kind": "step",
            "phase": "warmup",
            "end": {
                "type": "time",
                "seconds": 600
            },
            "target": {
                "type": "none"
            }
        },
        {
            "id": "rep-1",
            "kind": "repeat",
            "iterations": 6,
            "steps": [
                {
                    "id": "w-1",
                    "kind": "step",
                    "phase": "interval",
                    "end": {
                        "type": "distance",
                        "meters": 1000
                    },
                    "target": {
                        "type": "hr_zone",
                        "zone": 4
                    }
                },
                {
                    "id": "r-1",
                    "kind": "step",
                    "phase": "recovery",
                    "end": {
                        "type": "time",
                        "seconds": 90
                    },
                    "target": {
                        "type": "none"
                    }
                }
            ]
        },
        {
            "id": "cd-1",
            "kind": "step",
            "phase": "cooldown",
            "end": {
                "type": "time",
                "seconds": 600
            },
            "target": {
                "type": "hr_zone",
                "zone": 2
            }
        }
    ]
}

Response

{
    "id": "ptp_…",
    "date": "2026-08-25",
    "sport": "running",
    "title": "Tempo 8k",
    "description": "<p>Easy warmup, then tempo.</p>",
    "distance_km": 10,
    "duration_s": 3600,
    "is_draft": false,
    "is_locked": false,
    "structured_workout": null,
    "updated_at": "2026-08-25 12:01:03",
    "version": 3,
    "partner_app_id": "ptapp_…"
}

DELETE /plans/{id}/structure — снимает структуру, день остаётся. Те же права и CAS.

POST/api/partner/v1/plans/{id}/publish

Снимает is_draft. Повтор на уже опубликованном дне идемпотентен (тот же объект). Лок → 403 plan_locked.

Bearer · plans:write + plans:publish

Response

{
    "id": "ptp_…",
    "date": "2026-08-25",
    "sport": "running",
    "title": "Tempo 8k",
    "description": "<p>Easy warmup, then tempo.</p>",
    "distance_km": 10,
    "duration_s": 3600,
    "is_draft": false,
    "is_locked": false,
    "structured_workout": null,
    "updated_at": "2026-08-25 12:01:03",
    "version": 3,
    "partner_app_id": "ptapp_…"
}

Запись структуры не пушит тренировку на Garmin/Wahoo. Доставка на часы — действие атлета в PaceTrack.

GET/api/partner/v1/workouts

Завершённые тренировки внутри скользящего окна (дефолт 30 дней от сейчас, таймзона приложения).

Bearer · workouts:read

QueryОписание
fromYYYY-MM-DD или DATETIME, начало окна по start_time. Дефолт — отсечка 30 дней. Старше окна → 400 lookback_exceeded. Не ISO-формат → 400 invalid_request.
toYYYY-MM-DD или DATETIME, верхняя граница start_time. Для даты без времени — включительно до конца дня.
limit1–200, дефолт 50
afterКурсор ptw_…
updated_afterDATETIME по updated_at (иначе uploaded_at). Сортировка ASC; без него — start_time DESC.

Response

{
    "workouts": [
        {
            "id": "ptw_…",
            "source": "garmin",
            "source_activity_id": null,
            "sport": "running",
            "sport_label": "Run",
            "start_time": "2026-08-24 07:12:00",
            "distance_m": 10012.4,
            "duration_s": 3120,
            "avg_heart_rate": 148,
            "has_fit": true,
            "summary": {
                "distance_m": 10012.4,
                "duration_s": 3120,
                "avg_hr": 148
            },
            "updated_at": "2026-08-24 08:01:11",
            "data_edited": false,
            "data_edited_at": null,
            "data_edit_version": 0
        }
    ],
    "has_more": false
}

summary содержит сводные поля, доступные у источника тренировки. Запрос тренировки за пределами доступного окна по идентификатору возвращает 404. GPS-трек и ряды метрик доступны через GET …/track.

POST/api/partner/v1/workouts

Создаёт факт завершённой тренировки из JSON-summary партнёра. HTTP 201 при создании, 200 — если запрос оказался повтором (дедуп по external_id или тот же Idempotency-Key).

Bearer · workouts:write

Body

ПолеТипОписание
sportstringОбязательно. Код вида спорта из справочника (тот же, что у POST /v1/plans).
start_timestringОбязательно. Локальное время старта, YYYY-MM-DDTHH:MM:SS. Часовой пояс в строке игнорируется.
duration_sintОбязательно, > 0, максимум 259200 (3 суток).
moving_time_sintОпционально. Не может заметно превышать duration_s.
distance_mnumberОпционально, >= 0. Средняя скорость (distance_m/duration_s) не должна превышать 30 м/с — это проверка перепутанных единиц, не спортивного результата.
avg_heart_rate / max_heart_rateintОпционально, 1..300. max_heart_rate не может быть меньше avg_heart_rate.
elevation_gain_m / elevation_loss_mnumberОпционально, >= 0.
external_idstringДо 100 символов. Вместе с общим source=partner_api участвует в дедупе — рекомендуем свой namespace-префикс. Настоятельно рекомендуется: без него PaceTrack синтезирует ключ из спорта/времени/длительности/дистанции, и это менее надёжно для отличения похожих тренировок.
trackarrayОпционально, до 20000 точек. Каждая точка — объект с числовыми полями из фиксированного набора (time_s, moving_time_s, distance_m, lat, lon, altitude, heart_rate, cadence, power, speed_kmh, temperature); прочие поля точки отбрасываются. lat/lon — либо оба и в допустимом диапазоне, либо ни одного.
seriesobjectОпционально, до 20000 значений на ключ. Только известные ключи (time_s, moving_time_s, distance_m, heart_rate, altitude, cadence, power, speed_kmh, pace_sec_per_km, temperature) и только числа/null — неизвестные ключи отбрасываются.

Response

{
    "id": "ptw_…",
    "source": "garmin",
    "source_activity_id": null,
    "sport": "running",
    "sport_label": "Run",
    "start_time": "2026-08-24 07:12:00",
    "distance_m": 10012.4,
    "duration_s": 3120,
    "avg_heart_rate": 148,
    "has_fit": true,
    "summary": {
        "distance_m": 10012.4,
        "duration_s": 3120,
        "avg_hr": 148
    },
    "updated_at": "2026-08-24 08:01:11",
    "data_edited": false,
    "data_edited_at": null,
    "data_edit_version": 0
}

Принимает только наш JSON — не FIT/TCX/GPX. Не требует workouts:read. Идемпотентность: заголовок Idempotency-Key (окно 24 ч, как у POST /v1/plans) — независимо от него, повторная запись с тем же external_id тоже возвращает уже существующую тренировку, а не дубликат. Ошибки валидации — 400 invalid_request со списком errors: [{field, code}] (коды: required, invalid, unknown, too_long, too_large, too_many_points, in_future, too_old, exceeds_duration, below_avg, implausible_pace). Ваше приложение не получает собственный webhook workout.created о тренировке, которую только что создало этим запросом.

POST/api/partner/v1/workouts/fit-uploads

Ставит FIT-файл в очередь на разбор. HTTP 202 — файл принят, разбор ещё не завершён. Повтор с тем же external_id или Idempotency-Key возвращает ту же задачу вместо новой.

Bearer · workouts:write

Body

ПолеТипОписание
filemultipartОбязательно. Файл .fit, до 20 МБ. Только FIT — TCX/GPX/ZIP не принимаются.
external_idstringОпционально, form-поле или query, до 100 символов.

Response

{
    "job_id": "ptfu_…",
    "status": "pending"
}

Разбор асинхронный: HTTP-ответ не ждёт py-gateway — иначе недоступный парсер держал бы воркер PHP-FPM и мог уронить весь сайт, не только партнёрский API. Заберите итог через GET …/fit-uploads/{job_id}. Явный текст/структура через этот эндпоинт не передаются — только сам FIT. Файл больше нашего лимита возвращает 400 size, а не «файл не передан».

GET/api/partner/v1/workouts/fit-uploads/{job_id}

Статус задачи разбора: pending, running, done (с готовой тренировкой) или failed (с кодом ошибки). Чужой или несуществующий job_id — 404.

Bearer · workouts:write

Response

{
    "job_id": "ptfu_…",
    "status": "done",
    "workout": {
        "id": "ptw_…",
        "source": "garmin",
        "source_activity_id": null,
        "sport": "running",
        "sport_label": "Run",
        "start_time": "2026-08-24 07:12:00",
        "distance_m": 10012.4,
        "duration_s": 3120,
        "avg_heart_rate": 148,
        "has_fit": true,
        "summary": {
            "distance_m": 10012.4,
            "duration_s": 3120,
            "avg_hr": 148
        },
        "updated_at": "2026-08-24 08:01:11",
        "data_edited": false,
        "data_edited_at": null,
        "data_edit_version": 0
    }
}

{
    "job_id": "ptfu_…",
    "status": "failed",
    "error": {
        "code": "not_fit",
        "message": "FIT parse failed"
    }
}

Обычно результат готов в пределах минуты. Поллинг не тратит бюджет workouts:read — эта ручка требует только workouts:write.

GET/api/partner/v1/workouts/{id}

Один факт в том же JSON, что элемент списка.

Bearer · workouts:read

Response

{
    "id": "ptw_…",
    "source": "garmin",
    "source_activity_id": null,
    "sport": "running",
    "sport_label": "Run",
    "start_time": "2026-08-24 07:12:00",
    "distance_m": 10012.4,
    "duration_s": 3120,
    "avg_heart_rate": 148,
    "has_fit": true,
    "summary": {
        "distance_m": 10012.4,
        "duration_s": 3120,
        "avg_hr": 148
    },
    "updated_at": "2026-08-24 08:01:11",
    "data_edited": false,
    "data_edited_at": null,
    "data_edit_version": 0
}

GPS и ряды метрик — отдельный GET …/track, в карточку не входят.

GET/api/partner/v1/workouts/{id}/fit

Возвращает исходный FIT-файл или FIT-файл, сформированный PaceTrack для тренировки, записанной на телефон или Apple Watch. Если файл недоступен, возвращает 404 no_fit.

Bearer · workouts:read

Response

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="workout.fit"

<binary FIT>

has_fit имеет значение true, только если FIT-файл доступен в хранилище. Для тренировки без FIT-файла используйте GET …/track, чтобы получить доступные GPS-данные и ряды метрик.

Файл всегда исходный: если атлет или тренер исправили данные в PaceTrack (data_edited = true), исправления видны в карточке тренировки и в GET …/track, но не в FIT-файле. data_edit_version растёт при каждой правке и её отмене.

GET/api/partner/v1/workouts/{id}/track

Возвращает GPS-трек и доступные ряды метрик тренировки. Эндпоинт использует то же скользящее окно, что и остальные запросы тренировок; запрос за его пределами возвращает 404. При отсутствии GPS ответ имеет статус 200, has_gps: false и track: []. Требуются scopes workouts:read и workouts:track; при недостаточных правах возвращается 403 insufficient_scope.

Bearer · workouts:read + workouts:track

Response

{
    "id": "ptw_…",
    "source": "garmin",
    "source_activity_id": "17654321098",
    "source_url": null,
    "sport": "running",
    "start_time": "2026-08-27T11:07:49Z",
    "timezone": "Europe/Moscow",
    "distance_m": 3408.8,
    "duration_s": 1129,
    "moving_time_s": 1129,
    "avg_heart_rate": 122,
    "max_heart_rate": 139,
    "elevation_gain_m": 19,
    "elevation_loss_m": 12,
    "has_gps": true,
    "is_manual": false,
    "is_indoor": false,
    "is_treadmill": false,
    "track": [
        {
            "time_s": 0,
            "moving_time_s": 0,
            "distance_m": 0.6,
            "lat": 59.212355,
            "lon": 37.101288
        }
    ],
    "series": {
        "time_s": [
            0,
            1,
            2
        ],
        "moving_time_s": [
            0,
            1,
            2
        ],
        "distance_m": [
            0.6,
            3.1,
            5.8
        ],
        "heart_rate": [
            110,
            118,
            122
        ],
        "altitude": [
            118.4,
            118.6,
            118.5
        ],
        "cadence": [
            160,
            164,
            166
        ],
        "power": [
            null,
            null,
            null
        ],
        "speed_kmh": [
            8.4,
            9.1,
            9
        ],
        "pace_sec_per_km": [
            428,
            396,
            400
        ],
        "temperature": [
            18,
            18,
            18
        ]
    }
}

start_time имеет формат ISO 8601 в UTC (Z). Время Polar без указанного часового пояса преобразуется в UTC с помощью start_time_utc_offset, заданного в минутах. timezone содержит часовой пояс IANA или null, если он не определён. Набор ключей series зависит от источника; массивы в примере сокращены. is_treadmill определяется для бега, ходьбы и хайкинга без GPS. В v1 поле source_url может быть null.

GET/api/partner/v1/wellness

Дневные строки wellness. Несколько источников на одну дату возможны (разные source).

Bearer · wellness:read

QueryОписание
fromYYYY-MM-DD. Дефолт — отсечка 30 дней.
toYYYY-MM-DD
limit1–400, дефолт 50
afterКурсор ptwl_…
updated_afterDATETIME. Сортировка updated_at ASC; без него — metric_date DESC.

Response

{
    "wellness": [
        {
            "id": "ptwl_…",
            "date": "2026-08-24",
            "source": "garmin",
            "readiness_score": 78,
            "readiness_state": "productive",
            "hrv_ms": 62.4,
            "resting_hr": 48,
            "sleep_score": 81,
            "sleep_state": "good",
            "sleep_duration_s": 27600,
            "sleep_efficiency_pct": 91,
            "load_score": 12.3,
            "load_state": "high",
            "updated_at": "2026-08-24 09:00:00"
        }
    ],
    "has_more": false
}

Числовые поля могут быть null, если источник их не отдал. source — garmin, polar, whoop, intervals и т.д.

GET/api/partner/v1/wellbeing

Дневник самочувствия, который ведёт сам атлет. Это не wellness с часов. Область отдельная: атлет отмечает её галочкой «данные о здоровье».

Bearer · wellbeing:read

QueryОписание
fromYYYY-MM-DD. Дефолт — отсечка истории приложения.
toYYYY-MM-DD
limit1–120, дефолт 50

Response

{
    "wellbeing": [
        {
            "id": "ptwb_example",
            "date": "2026-10-02",
            "sleep_quality": 4,
            "sleep_min": 450,
            "feeling": 3,
            "stress": 2,
            "soreness": 2,
            "mood": null,
            "nutrition": null,
            "weight_kg": null,
            "is_sick": false,
            "is_injured": false,
            "note": null,
            "origin": "athlete"
        }
    ],
    "has_more": false
}

Шкалы 1–5. origin=intervals — день заполнен из Intervals.icu, атлет сам его не сохранял; такие значения меняются вместе с Intervals.icu. Вебхук wellbeing.updated не содержит значений, только ссылку на этот ресурс.

structured_workout

Единственный формат структуры — JSON PaceTrack. Не Garmin XML, не ZWO, не Intervals.icu. Бег: schema: 1. Вело: schema: 2 и опционально environment (indoor / outdoor). Лыжные гонки (xc_ski): schema: 3, цели только по пульсу и RPE, темпа и мощности нет. Поле schema обязательно. Максимум 50 исполняемых шагов с разворотом repeat; вложенность repeat — 1 уровень; iterations 2–99.

{
    "schema": 1,
    "sport": "running",
    "steps": [
        {
            "id": "wu-1",
            "kind": "step",
            "phase": "warmup",
            "end": {
                "type": "time",
                "seconds": 600
            },
            "target": {
                "type": "none"
            }
        },
        {
            "id": "rep-1",
            "kind": "repeat",
            "iterations": 6,
            "steps": [
                {
                    "id": "w-1",
                    "kind": "step",
                    "phase": "interval",
                    "end": {
                        "type": "distance",
                        "meters": 1000
                    },
                    "target": {
                        "type": "hr_zone",
                        "zone": 4
                    }
                },
                {
                    "id": "r-1",
                    "kind": "step",
                    "phase": "recovery",
                    "end": {
                        "type": "time",
                        "seconds": 90
                    },
                    "target": {
                        "type": "none"
                    }
                }
            ]
        },
        {
            "id": "cd-1",
            "kind": "step",
            "phase": "cooldown",
            "end": {
                "type": "time",
                "seconds": 600
            },
            "target": {
                "type": "hr_zone",
                "zone": 2
            }
        }
    ]
}
ПолеЗначения
kindstep | repeat
phasewarmup | interval | recovery | cooldown
end.typetime (seconds) | distance (meters) | lap
target.typenone | hr_zone (1–5) | hr (min_bpm, max_bpm) | pace_zone | pace (slow_s_per_km, fast_s_per_km). Вело дополнительно: мощность/каденс в schema 2. Лыжи (schema 3): none | hr_zone (I1–I5 лыжной шкалы) | hr | rpe (min_rpe, max_rpe — целые 1–10).

Webhooks

Подписка регистрируется для приложения с помощью client credentials. Webhook содержит метаданные события и data_url для получения ресурса. События отправляются только для атлетов с активной OAuth-авторизацией и в пределах предоставленных scopes. События plan.* для черновиков не отправляются. URL должен использовать HTTPS и не может быть IP-адресом.

PUT/api/partner/v1/apps/webhook

Зарегистрировать или сменить URL и список событий. Сразу запускает challenge-response.

HTTP Basic client_id:client_secret

Body

{
    "url": "https://example.com/hooks/pacetrack",
    "events": [
        "plan.updated",
        "workout.created",
        "wellness.updated"
    ]
}

Response

{
    "ok": true,
    "verified": true,
    "events": [
        "plan.updated",
        "workout.created",
        "wellness.updated"
    ]
}

Алиас поля URL: webhook_url. Если events нет — подписка на полный список v1. POST на тот же путь эквивалентен PUT. Неуспешная проверка → 400, события не идут.

GET/api/partner/v1/apps/webhook

Текущая подписка.

HTTP Basic

Response

{
    "url": "https://example.com/hooks/pacetrack",
    "verified": true,
    "verified_at": "2026-08-25 10:00:00",
    "status": "verified",
    "error": null,
    "events": [
        "plan.updated",
        "workout.created"
    ]
}

status: none | unverified | verified | failing.

DELETE/api/partner/v1/apps/webhook

Снять подписку.

HTTP Basic

Response

{
    "ok": true
}

POST/api/partner/v1/apps/webhook/verify

Повторно проверить уже сохранённый URL.

HTTP Basic

Response

{
    "ok": true,
    "verified": true
}

POST/api/partner/v1/apps/webhook/test

Тестовый ping только если URL уже verified. Иначе 400 webhook_unverified.

HTTP Basic

Response

{
    "ok": true
}

События v1

СобытиеКогда
plan.createdСоздан опубликованный день плана
plan.updatedОпубликованные карточка или структура изменились
plan.publishedЧерновик опубликован
plan.deletedУдалён опубликованный день
workout.createdСоздана завершённая тренировка
workout.updatedДанные завершённой тренировки изменены
workout.deletedФакт удалён
wellness.updatedСтрока wellness создана или пересчитана
wellbeing.updatedЗапись дневника самочувствия создана, изменена или удалена. В теле нет значений — только data_url. Нужна область wellbeing:read.
connection.revokedАтлет, партнёр или админ отключили приложение
webhook.verifyТолько handshake, не подписка
webhook.testРучной test ping

Тело webhook-запроса

{
    "spec_version": "1",
    "event": "plan.updated",
    "event_id": "a1b2c3…",
    "occurred_at": "2026-08-24T12:01:03+03:00",
    "app_id": "ptapp_…",
    "user_id": "ptu_…",
    "resource_type": "plan",
    "resource_id": "ptp_…",
    "data_url": "https://pacetrack.ru/api/partner/v1/plans/ptp_…"
}
  • X-PaceTrack-Signature: sha256=<hex> — HMAC-SHA256 сырого тела на webhook_signing_secret
  • X-PaceTrack-Delivery-Id — id попытки (дедуп)
  • User-Agent: PaceTrack-Webhook/1.0

Получатель должен вернуть ответ 2xx в течение 5 секунд. При ошибке PaceTrack повторяет доставку через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Фактическое время отправки может отличаться до 6 минут из-за интервала обработки очереди. После исчерпания попыток подписка получает статус failing.

Проверка URL

Для проверки PaceTrack отправляет POST-запрос на зарегистрированный URL с заголовком X-PaceTrack-Webhook-Verification: 1. Получатель должен вернуть HTTP 2xx и JSON с исходным значением challenge без изменений. Запрос подписывается так же, как обычное событие.

{
    "spec_version": "1",
    "event": "webhook.verify",
    "challenge": "<random>",
    "app_id": "ptapp_…"
}

Ошибки

{
    "error": "conflict",
    "message": "Plan was modified",
    "request_id": "1b408196ec2882df",
    "updated_at": "2026-08-25 12:01:03",
    "version": 4
}
errorHTTPКогда
unauthorized401Bearer-токен отсутствует, истёк или отозван
invalid_client401/403Неверный client_secret или приложение неактивно
invalid_grant400Недействительный authorization code, PKCE verifier или refresh-токен
invalid_request400Неверные параметры, в т.ч. timestamp If-Match
invalid_structure400Валидатор структуры, поле errors
lookback_exceeded400from старше окна истории
webhook_unverified400test ping до успешного verify
not_found404Нет ресурса или чужой черновик
no_fit404Нет FIT-файла, encode недоступен, или ручной ввод без трека
insufficient_scope403Нет нужного scope, в т.ч. workouts:track на GET …/track
plan_locked403Лок дня, недели или атлета
publish_required403Нет plans:publish на опубликованный день
user_limit_exceeded403Лимит подключённых атлетов приложения
conflict409version не совпал
rate_limited429Лимит запросов или token anti-bruteforce
method405Метод не для этого пути
not_ready503Схема Partner API ещё не применена