API PrimePlanner связывает мероприятия организации с её системами: CRM, сайтом, учётной системой. Через API можно читать мероприятия, программу, участников, заявки, заказы и отметки входа, добавлять участников и решать по заявкам. Вебхуки сообщают о новых участниках, заявках, оплатах и входах. API и вебхуки доступны на тарифах «Бизнес» и «Корпоративный».
Запросы
Адрес API — https://my.primeplanner.app/api/v1. Ключ передаётся в заголовке Authorization: Bearer pp_… и действует с правами роли, выбранной при его создании.
curl https://my.primeplanner.app/api/v1/activities \
-H "Authorization: Bearer pp_…"
- Списки отдаются страницами: параметр
limit — до 200 записей (по умолчанию 50), параметр cursor — значение next_cursor предыдущей страницы. Если next_cursor равен null, страница последняя. - Время передаётся в формате ISO 8601 в UTC, суммы — целым числом копеек.
- Запросы, которые меняют данные, принимают заголовок
Idempotency-Key со случайным UUID: повтор с тем же ключом и тем же телом в течение 48 часов возвращает сохранённый ответ и не повторяет изменение. - Один ключ выполняет до 600 запросов в минуту. Сверх лимита API отвечает 429 с заголовком
Retry-After. - Ошибки приходят с кодом HTTP и телом
{ "statusCode": 404, "data": { "message": "…", "code": "not_found" } }. Код plan_required означает, что тариф организации не включает API.
Участники добавляются пачкой до 500 человек. Профиль ищется по external_id — ID человека в вашей системе, затем по телефону, адресу почты и совпадающему ФИО. Если профиль не найден, PrimePlanner создаёт его.
curl -X POST https://my.primeplanner.app/api/v1/activities/<id>/participants \
-H "Authorization: Bearer pp_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a0e-9d1b-4c47-9a44-2f8e3c1d5b7a" \
-d '{"participants":[{"external_id":"crm-1042","lastname":"Иванова","firstname":"Анна","email":"anna@example.com"}]}'
Вебхуки
PrimePlanner отправляет событие POST-запросом с телом JSON { "id", "type", "created", "data" }. Заголовок X-PrimePlanner-Event содержит тип события, X-PrimePlanner-Delivery — ID доставки, X-PrimePlanner-Signature — подпись вида t=<время>,v1=<подпись>. Подпись — HMAC-SHA256 секрета адреса от строки <время>.<тело> в шестнадцатеричном виде.
Ответьте кодом 2xx в течение 10 секунд. Если адрес не ответил или вернул ошибку, PrimePlanner повторяет отправку через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч. После 10 неудачных доставок подряд адрес выключается. Одно событие может прийти дважды: сверяйте поле id.
| participant.created | Участник добавлен в мероприятие |
| participant.updated | Группы или внешний ID участника изменены |
| participant.deleted | Участник удалён из мероприятия |
| registration.created | Новая заявка на участие |
| registration.approved | Заявка одобрена |
| registration.rejected | Заявка отклонена |
| order.paid | Заказ билета оплачен или оформлен бесплатно |
| order.refunded | Возврат по заказу |
| checkin.created | Участник отмечен на входе или на событии |
Проверка подписи
Node.js
import crypto from "node:crypto";
// header — значение X-PrimePlanner-Signature, body — тело запроса как строка, без разбора JSON
export function verify(secret, header, body) {
const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex"), "hex");
const given = Buffer.from(v1, "hex");
return expected.length === given.length && crypto.timingSafeEqual(expected, given);
}
PHP
function verify(string $secret, string $header, string $body): bool {
parse_str(str_replace(',', '&', $header), $parts);
if (!isset($parts['t'], $parts['v1']) || abs(time() - (int) $parts['t']) > 300) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret);
return hash_equals($expected, $parts['v1']);
}
Python
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes) -> bool:
parts = dict(item.split("=", 1) for item in header.split(",") if "=" in item)
if "t" not in parts or "v1" not in parts or abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Запросы API
GET/activities
Мероприятия организации
Право роли ключа: manage_events, activity_schedule или activity_participants.
Параметры
limit · в строке запросаcursor · в строке запроса
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].title* | string | null | |
| data[].intro* | string | null | |
| data[].start_date* | string (date-time) | null | |
| data[].archived* | boolean | |
| data[].sections* | string[] | |
| data[].created* | string (date-time) | |
| next_cursor* | string (uuid) | null | Курсор следующей страницы или null |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}
Мероприятие
Право роли ключа: manage_events, activity_schedule или activity_participants.
Параметры
id · в пути — ID мероприятия
Ответ 200
| Поле | Тип | Описание |
|---|
| id* | string (uuid) | |
| title* | string | null | |
| intro* | string | null | |
| start_date* | string (date-time) | null | |
| archived* | boolean | |
| sections* | string[] | |
| created* | string (date-time) | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/events
Программа мероприятия
Право роли ключа: activity_schedule или activity_participants.
Параметры
id · в пути — ID мероприятия
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].activity_id* | string (uuid) | |
| data[].kind* | "talk" | "discussion" | "workshop" | "sport" | "show" | "meal" | "logistics" | "excursion" | "other" | |
| data[].title* | string | null | |
| data[].intro* | string | null | |
| data[].start* | string (date-time) | |
| data[].end* | string (date-time) | |
| data[].time_zone* | string | null | IANA-зона места |
| data[].location* | string | null | |
| data[].latitude* | number | null | |
| data[].longitude* | number | null | |
| data[].registration_open* | boolean | |
| data[].capacity* | integer | null | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/groups
Группы участников
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятия
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].title* | string | null | |
| data[].description* | string | null | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/participants
Участники с контактами
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияlimit · в строке запросаcursor · в строке запроса
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].activity_id* | string (uuid) | |
| data[].profile_id* | string (uuid) | null | |
| data[].external_id* | string | null | |
| data[].lastname* | string | null | |
| data[].firstname* | string | null | |
| data[].patronymic* | string | null | |
| data[].phone* | string | null | E.164 |
| data[].email* | string | null | |
| data[].company* | string | null | |
| data[].position* | string | null | |
| data[].group_ids* | string (uuid)[] | |
| data[].ticket_type_id* | string (uuid) | null | |
| data[].order_id* | string (uuid) | null | |
| data[].source* | "manual" | "self" | |
| data[].checked_in_at* | string (date-time) | null | Первый вход по QR-билету |
| next_cursor* | string (uuid) | null | Курсор следующей страницы или null |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
POST/activities/{id}/participants
Добавить участников
Права роли ключа: activity_participants и manage_profiles. Профиль ищется по external_id, телефону, адресу почты, затем по совпадающему ФИО; иначе создаётся. Данные найденного профиля не меняются. До 500 участников за запрос.
Параметры
id · в пути — ID мероприятияIdempotency-Key · заголовок — Повтор с тем же ключом и телом возвращает сохранённый ответ (48 ч)
Тело запроса
| Поле | Тип | Описание |
|---|
| participants* | object[] | |
| participants[].external_id | string | null | ID человека в вашей системе: по нему находится профиль |
| participants[].lastname* | string | |
| participants[].firstname* | string | |
| participants[].patronymic | string | null | |
| participants[].phone | string | null | Номер в формате E.164 или российский номер |
| participants[].email | string | null | |
| participants[].company | string | null | |
| participants[].position | string | null | |
| participants[].group_ids | string (uuid)[] | |
Ответ 200
| Поле | Тип | Описание |
|---|
| created* | integer | Новых профилей |
| matched* | integer | Найдено существующих профилей |
| added* | integer | Добавлено в мероприятие |
| already* | integer | Уже были участниками |
| participants* | object[] | |
| participants[].row* | integer | |
| participants[].participant_id* | string (uuid) | |
| participants[].profile_id* | string (uuid) | |
| rejected* | object[] | |
| rejected[].row* | integer | |
| rejected[].reason* | string | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/participants/{participant_id}
Участник
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияparticipant_id · в пути
Ответ 200
| Поле | Тип | Описание |
|---|
| id* | string (uuid) | |
| activity_id* | string (uuid) | |
| profile_id* | string (uuid) | null | |
| external_id* | string | null | |
| lastname* | string | null | |
| firstname* | string | null | |
| patronymic* | string | null | |
| phone* | string | null | E.164 |
| email* | string | null | |
| company* | string | null | |
| position* | string | null | |
| group_ids* | string (uuid)[] | |
| ticket_type_id* | string (uuid) | null | |
| order_id* | string (uuid) | null | |
| source* | "manual" | "self" | |
| checked_in_at* | string (date-time) | null | Первый вход по QR-билету |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
PATCH/activities/{id}/participants/{participant_id}
Группы и внешний ID участника
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияparticipant_id · в путиIdempotency-Key · заголовок — Повтор с тем же ключом и телом возвращает сохранённый ответ (48 ч)
Тело запроса
| Поле | Тип | Описание |
|---|
| group_ids | string (uuid)[] | Новый набор групп участника |
| external_id | string | null | |
Ответ 200
| Поле | Тип | Описание |
|---|
| id* | string (uuid) | |
| activity_id* | string (uuid) | |
| profile_id* | string (uuid) | null | |
| external_id* | string | null | |
| lastname* | string | null | |
| firstname* | string | null | |
| patronymic* | string | null | |
| phone* | string | null | E.164 |
| email* | string | null | |
| company* | string | null | |
| position* | string | null | |
| group_ids* | string (uuid)[] | |
| ticket_type_id* | string (uuid) | null | |
| order_id* | string (uuid) | null | |
| source* | "manual" | "self" | |
| checked_in_at* | string (date-time) | null | Первый вход по QR-билету |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
DELETE/activities/{id}/participants/{participant_id}
Удалить участника из мероприятия
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияparticipant_id · в путиIdempotency-Key · заголовок — Повтор с тем же ключом и телом возвращает сохранённый ответ (48 ч)
Ответ 200
| Поле | Тип | Описание |
|---|
| deleted* | boolean | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/registrations
Заявки на участие
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияlimit · в строке запросаcursor · в строке запросаstatus · в строке запроса
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].activity_id* | string (uuid) | |
| data[].profile_id* | string (uuid) | |
| data[].status* | "pending" | "approved" | "rejected" | |
| data[].event_ids* | string (uuid)[] | |
| data[].ticket_type_id* | string (uuid) | null | |
| data[].comment* | string | null | |
| data[].created* | string (date-time) | |
| data[].decided* | string (date-time) | null | |
| next_cursor* | string (uuid) | null | Курсор следующей страницы или null |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
POST/activities/{id}/registrations/{registration_id}/approve
Одобрить заявку
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияregistration_id · в путиIdempotency-Key · заголовок — Повтор с тем же ключом и телом возвращает сохранённый ответ (48 ч)
Ответ 200
| Поле | Тип | Описание |
|---|
| joined* | string (uuid)[] | |
| skipped* | string (uuid)[] | |
| awaiting_payment* | boolean | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
POST/activities/{id}/registrations/{registration_id}/reject
Отклонить заявку
Право роли ключа: activity_participants.
Параметры
id · в пути — ID мероприятияregistration_id · в путиIdempotency-Key · заголовок — Повтор с тем же ключом и телом возвращает сохранённый ответ (48 ч)
Тело запроса
| Поле | Тип | Описание |
|---|
| comment | string | null | |
Ответ 200
| Поле | Тип | Описание |
|---|
| rejected* | boolean | |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/orders
Заказы билетов
Право роли ключа: activity_tickets.
Параметры
id · в пути — ID мероприятияlimit · в строке запросаcursor · в строке запроса
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].activity_id* | string (uuid) | |
| data[].profile_id* | string (uuid) | |
| data[].ticket_type_id* | string (uuid) | |
| data[].status* | "pending_payment" | "pending_invoice" | "paid" | "cancelled" | "expired" | "refunded" | "partially_refunded" | |
| data[].method* | "card" | "invoice" | "free" | "manual" | |
| data[].amount* | integer | Копейки |
| data[].refunded_amount* | integer | |
| data[].currency* | string | |
| data[].created* | string (date-time) | |
| data[].paid_at* | string (date-time) | null | |
| next_cursor* | string (uuid) | null | Курсор следующей страницы или null |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)
GET/activities/{id}/checkins
Отметки входа
Право роли ключа: activity_checkin или activity_participants.
Параметры
id · в пути — ID мероприятияlimit · в строке запросаcursor · в строке запроса
Ответ 200
| Поле | Тип | Описание |
|---|
| data* | object[] | |
| data[].id* | string (uuid) | |
| data[].participant_id* | string (uuid) | |
| data[].event_id* | string (uuid) | null | null — вход на мероприятие |
| data[].method* | "qr" | "manual" | |
| data[].checked_at* | string (date-time) | |
| next_cursor* | string (uuid) | null | Курсор следующей страницы или null |
Ошибки
- 401 — Нет ключа или ключ отозван (`api_key_required`, `invalid_api_key`)
- 403 — Нет права у роли ключа или тариф без API (`plan_required`)
- 404 — Не найдено (`not_found`)
- 429 — Больше 600 запросов в минуту (`rate_limited`, заголовок Retry-After)