Коллбэки и webhooks

Что такое webhook

Webhook — механизм получения уведомлений об определённых событиях на TimePad (в основном о действиях пользователей) на свой собственный сайт.

Например, на TimePad есть webhook, который позволяет отслеживать изменения статусов регистрации пользователей. Он вызывается каждый раз при изменении любого билета вашей организации. По сути происходит вот что: данные регистрации кладутся в JSON и отправляются на URL, указаный в настройках организации.

Как настроить webhook

Для использования достаточно прописать в настройках организации URL, на который будет отправляться POST-запрос.

image

В поле Ссылка для уведомления нужно оставить адрес, к которому TimePad будет обращаться в случае смены статусов билетов, а в поле Секретная фраза — ключ, которым будет подписано сообщение от TimePad. И не забудьте нажать «Сохранить».

После этого при любой смене статуса билета в вашей организации — например, при регистрации или оплате — будет выполняться POST-запрос на адрес, который вы указали в настройках.

Как прочитать webhook

Timepad посылает хук методом POST с JSON-объектом в теле запроса. Также мы подписываем хуки с помощью hmac, что позволяет вашим приложениям убедиться, что данные посылаем именно мы.

Пример чтения webhook'а в PHP

<?php
// Читаем содержимое POST-запроса
$body      = @file_get_contents("php://input");
// Генерируем код, которым должен быть подписан webhook
$sha1      = hash_hmac('sha1', $body, 'SuperSecret');

// Сравниваем полученый код подписи, с тем что пришёл с хуком
if ("sha1={$sha1}" == $_SERVER['HTTP_X_HUB_SIGNATURE']) {
    // webhook пришёл от сервиса Timepad
    print_r(json_decode($body, true));
}
?>

Типы вебхуков

Сейчас существует 3 типа вебхуков:

  • по изменению статуса билета
  • по изменению статуса заказа
  • по изменению события

Использование вебхуков по изменению статуса заказа и билета

Вебхуки по заказу и билетам связаны между собой. При любом измении заказа посылается вебхук по заказу и вебхуки по каждому из билетов в заказе.

Данные вебхуки посылаются в случаях:

  • Создание заказа (в том числе бронирование для платных билетов и подача заявки на участие при включенном режиме "Прием заявок")
  • Оплата заказа
  • Отказ от заказа покупателем (в том числе отказ от заказа с забронированными билетами)
  • Отмена возврата заказа
  • Возврат денег за оплаченный заказ (в момент установки метки "Возвращен" для заказа)
  • Истечение срока брони билетов в заказе
  • Отклонение или приём заявки на участие в событии
  • Удаление заказа
  • Перенос оплаты с одного заказа на другой (вебхуки будут посланы по обоим заказам)
  • Изменение ответов анкеты регистрации

Содержание JSON запроса для хука изменения статусов заказа

Структура аналогична ответу API по заказам OAuth2. Пример такого json'a:

{
    "id": 4955686, // Id заказа
    "created_at": "2019-05-23T14:34:41+0300", // Дата создания заказа
    "status": {
        "name": "ok", // Идентификатор статуса
        "title": "бесплатно" // Человекочитаемый статус заказа
    },
    "mail": "test-mail@ya.ru", // Адрес электронной почты заказчика билетов
    "payment": {
        "amount": 0 // Оплаченная сумма (количество рублей, отданных покупателем платёжной системе в этом заказе)
    },
    "tickets": [
        {
            "id": 2, // Id билета
            "number": "2:53392596", // Номер билета
            "price_nominal": "0", // Номинальная цена билета на момент покупки
            "answers": {}, // Объект с ответами на вопросы анкеты
            "ticket_type": {
                "id": 10, // Id типа билета
                "name": "Входной билет", // Название типа билета
                "description": "Входной билет на событие", // Описание типа билета
                "buy_amount_min": 1, // Минимальное количество билетов в одной покупке
                "buy_amount_max": 30, // Максимальное количество билетов в одной покупке
                "price": 0, // Цена билета
                "is_promocode_locked": false, // Закрыт ли этот тип билета введённым промокодом
                "remaining": 28, // Сколько билетов осталось
                "sale_ends_at": "2019-05-30T19:30:00+0300", // Дата окончания продажи типа билета
                "is_active": true, // Активность типа билета
                "ad_partner_profit": 0, // Партнёрская прибыль
                "send_personal_links": true, // Отправка персональных сссылок
                "status": "late" // Статус типа билета
            },
            "attendance": {}, // Объект информации о посещении пользователя с этим билетом
            "place": {}, // Объект информации о месте билета
            "codes": {
                "ean13": "1000533925961", // ean13 код билета
                "ean8": "53392596", // ean8 код билета
                "printed_code": "1000533925961" // Код, который будет напечатан на билете
            },
            "personal_link": false // Персональная ссылка
        }
    ],
    "answers": {
        "order_mail": "test-mail@ya.ru" // ответы
    },
    "promocodes": [], // Список промокодов
    "event": {
        "id": 2 // Id События
    },
    "referrer": {
        "campaign": "Прямой трафик", // Кампания
        "medium": "Прямой трафик", // Канал
        "source": "Прямой трафик" // Источник
    },
    "subscribed_to_newsletter": false // Подписка на анонсы событий организатора
}

Содержание JSON запроса для хука изменения статусов билетов

{
  "id": "5184211:83845994", // Номер билета (печататется на самом билете)
  "event_id": 215813, // ID мероприятия
  "organization_id": 29963, // ID организации, создавшей мероприятие
  "order_id": "4955686", // ID заказа
  "reg_date": "2015-07-24 19:04:37", // Дата заказа билета
  "reg_id": 361138, // Id типа билета
  "status": "забронировано", // Человекочитаемый статус заказа
  "status_raw": "booked", // Идентификатор статуса заказа 
  "email": "test-mail@ya.ru", // E-mail заказчика
  "surname": "Смирнов", // Фамилия на билете
  "name": "Владимир", // Имя на билете
  "attended": false, // Отметка о посещении мероприятия
  "code": "83845994", // Числовой код билета
  "barcode": "1000838459949", // Числовой код в формате EAN-13, напечатан на билете в виде штрих-кода
  "price_nominal": 1000, // Стоимость билета на момент заказа 
  "answers": [ // Список ответов на вопросы анкеты (если есть)
    {
      "id": 889802, // ID вопроса
      "type": "text", // Тип вопроса
      "name": "E-mail", // Текст вопроса
      "mandatory": null, // Обязательность вопроса
      "value": "test-mail@ya.ru" // Текст ответа
    },
    {
      "id": 889803, // ID вопроса
      "type": "text", // Тип вопроса
      "name": "Фамилия", // Текст вопроса
      "mandatory": null, // Обязательность вопроса
      "value": "Смирнов" // Текст ответа
    },
    {
      "id": 889804, // ID вопроса
      "type": "text", // Тип вопроса
      "name": "Имя", // Текст вопроса
      "mandatory": null, // Обязательность вопроса
      "value": "Владимир" // Текст ответа
    }
  ],
  "aux": []
}

Список возможных статусов билетов

Поля status:name/status_raw в запросе могут содержать следующие значения (в скобках указаны соответствующие значения поля status:title/status)

  • ok (ок): бесплатный билет успешно заказан
  • paid (оплачено): платный билет успешно оплачен он-лайн
  • booked (забронировано): билет находится в статусе "Забронировано"
  • notpaid (просрочено): билет не был оплачен и срок брони для него истек
  • inactive (отказ): участник отказался от участия
  • deleted (удалено): организатор удалил билет
  • returned (возврат): участнику были возвращены деньги за билет
  • pending (заявка рассматривается): заявка на участие в событии находится на рассмотрении
  • rejected (отклонено): заявка на участие в событии была отклонена
  • booked_offline (бронь для выкупа): билет был заказан для выкупа в офисе организатора
  • paid_offline (оплачено на месте): билет был оплачен в офисе организатора
  • paid_ur (оплачено компанией): билет был оплачен юридическим платежом
  • transfer_payment (перенесена оплата): билет был оплачен переносом оплаты с другого заказа
  • return_payment_request (запрос на возврат): участник оплатил билет и запросил возврат средств
  • return_payment_reject (отказ возврата): билет оплачен, участнику отказали в возврате
  • return_org (возврат организатором): билет оплачен, организатор возвращает деньги за билет
  • return_tp (возврат TimePad): билет оплачен, участнику будет сделан возврат

Как отвечать на webhook

Принимающий хуки скрипт в случае успеха должен отвечать http-кодом 200. При получении любого другого кода TimePad считает попытку отправки данных неудачной. Также отправка считается неудачной, если ответа не поступило в течение 5 секунд.

В таких случаях запрос будет отправлен на этот же адрес с теми же данными через час. Если эта попытка тоже не удалась, следующая состоится через сутки. После этого запрос будет удалён из очереди, даже если доставка так и не состоялась. Следите за своими серверами ;)

Проектирование системы, принимающей вебхуки

Разрабатывая систему, принимающую вебхуки, лучше снадбите её логами — какие вебхуки приходили.

Обязательно предусмотрите у себя следующие свойства вебхуков (кстати, все это относится к вебхукам абсолютно любых систем):

Порядок прихода вебхуков не обязательно хронологический

Вебхуки отправляются в множество потоков. Совершенно не исключен тот факт, что какой-то из потоков отправит более поздний вебхук быстрее, чем соседий поток — более ранний. Если нарушение хронологии может вам что-то сильно сломать — проверяйте дату хука (поле hook_generated_at), сравнивайте её с датой последнего хука по той же сущности, который вы приняли.

Вебхук может не прийти вовсе

  • Проблема может случиться на вашей стороне и вебхук не будет обработан
  • Проблема может случиться на нашей стороне (например, вебхук может быть в принципе не отправлен)
  • Проблема может случиться где-то посередине (например, проблемы в сети, блокировки на уровне магистральных провайдеров, сбившиеся таблицы маршрутизации и так далее)

Когда отправитель вебхука видит, что отправленный запрос не завершился корректно (HTTP статусом 200 OK), он его обычно переотправляет (еще одно место где может сбиться хронология). Вторая отправка чаще всего завершается успехом.

Один и тот же вебхук может прийти несколько раз

Достаточно часто бывает так, что принимающая сторона на самом деле обработала вебхук и ответила корректным статусом, но отправитель этого ответа не дождался или интерпретировал его неверно (например, при проблемах сети).

В этом случае отправитель сделает повторную попытку отправки вебхука и принимающая сторона будет обязана обработать его корректно – т.е. проигнорировать, но отправить ответ HTTP 200 OK. Вы можете проверять GUID вебхука (поле hook_guid).

При отправке вебхука есть таймаут

Наш сервер 5 секунд ждет ответа о том, что вебхук обработан. Если вам этого времени может не хватать (много обработки, медленное соединение, высокая нагрузка), используйте механизм очередей — приняв вебхук, ставьте его обработку в очередь и отвечайте, что вебхук принят.