API для разработчиков

API PrimePlanner связывает мероприятия организации с её системами: CRM, сайтом, учётной системой. Через API можно читать мероприятия, программу, участников, заявки, заказы и отметки входа, добавлять участников и решать по заявкам. Вебхуки сообщают о новых участниках, заявках, оплатах и входах. API и вебхуки доступны на тарифах «Бизнес» и «Корпоративный».

Как создать ключ и адрес вебхука в кабинете, описано в документации. Описание в формате OpenAPI 3.1 для Postman, Insomnia и генераторов клиентов — openapi.json.

Запросы

Адрес 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 | nullIANA-зона места
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 | nullE.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_idstring | nullID человека в вашей системе: по нему находится профиль
participants[].lastname*string
participants[].firstname*string
participants[].patronymicstring | null
participants[].phonestring | nullНомер в формате E.164 или российский номер
participants[].emailstring | null
participants[].companystring | null
participants[].positionstring | null
participants[].group_idsstring (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 | nullE.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_idsstring (uuid)[]Новый набор групп участника
external_idstring | 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 | nullE.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 ч)
Тело запроса
ПолеТипОписание
commentstring | 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) | nullnull — вход на мероприятие
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)