Webhooks

From CasperTech Wiki
Revision as of 03:34, 20 February 2018 by D1cd5b71-6209-4595-9bf0-771bf689ce00 (talk | contribs) (Created page with "New for 2018, our Webhook system is a beefed-up version of CasperVend's ANS notification system. This is an advanced system which requires programming knowledge. It's designed...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

New for 2018, our Webhook system is a beefed-up version of CasperVend's ANS notification system. This is an advanced system which requires programming knowledge. It's designed to reliably send events to servers you manage.

Benefits

* Reliable. If your server goes offline, we'll keep sending until it goes through (though see the caveats below)
* Not just for vendor sales, but can also notify for marketplace sales, updates and redeliveries
* JSON encoded so can be read by nearly all backend frameworks
* In the event of a failure to reach your endpoint, we will notify your avatar by IM (after 5 attempts).

Caveats

* If we can't get through to your server, we will try again on an exponential backoff. The first 5 attempts happen every minute, but after that the length of time doubles for each attempt.
* If we still cannot get through to your server after 43 attempts (approximately 24 hours), we will drop all pending notifications and disable your webhook.
* Because of the retry mechanism, your code must be prepared to accept duplicate requests.
* We send events in date order, so if an early event fails, you won't receive any later events until your endpoint responds correctly.
* Your endpoint must respond within 5 seconds.

Example Code

MySQL

Should work with any modern MySQL daemon. Tested with MariaDB 10.2.12

CREATE DATABASE IF NOT EXISTS `webhooks` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `webhooks`;

CREATE TABLE IF NOT EXISTS `events` (
  `eventID` char(36) NOT NULL,
  `received` datetime NOT NULL DEFAULT current_timestamp(),
  `processed` tinyint(1) unsigned NOT NULL DEFAULT 0,
  `data` mediumblob DEFAULT NULL,
  PRIMARY KEY (`eventID`),
  KEY `processed` (`processed`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

PHP

<?php

// Configuration options

define('WEBHOOK_SALT', 'YOUR SALT HERE');

define('ENABLE_DEBUG', true);
define('DEBUG_EMAIL', 'debug@caspertech.co.uk');

define('DATABASE_HOST', '127.0.0.1');
define('DATABASE_USER', 'webhooks');
define('DATABASE_PASSWORD', 'somepassword');
define('DATABASE_DATABASE', 'webhooks');

// Get the raw POST data
$postData = file_get_contents("php://input");

// Integrity check
if (!isset($_SERVER['HTTP_X_CASPER_WEBHOOK_INTEGRITY_HASH']) || $_SERVER['HTTP_X_CASPER_WEBHOOK_INTEGRITY_HASH'] !== sha1($postData) || !isset($_SERVER['HTTP_X_CASPER_WEBHOOK_VERIFY_HASH']))
{
    http_response_code(400);
    die('Corruption detected');
}

// Connect to the database
$dsn = 'mysql:dbname='.DATABASE_DATABASE.';host='.DATABASE_HOST;

try
{
    $dbh = new PDO($dsn, DATABASE_USER, DATABASE_PASSWORD);
} catch (PDOException $e)
{
    if (ENABLE_DEBUG) mail(DEBUG_EMAIL, 'Webhook Script Error', 'Database connection failed');
    die('Database connection failed');
}

// Now we check if the request is actually intended for us

$hashCheck = sha1(sha1($postData).":".WEBHOOK_SALT);

if ($_SERVER['HTTP_X_CASPER_WEBHOOK_VERIFY_HASH'] !== $hashCheck)
{
    http_response_code(403);
    if (ENABLE_DEBUG) mail(DEBUG_EMAIL, 'Webhook Script Error', 'Unauthorised request');
    die('Unauthorised');
}

// By this stage we have a validated webhook event. Now decode the json

$data = json_decode($postData, true);

if ($data === false)
{
    // JSON decode failure. This should never happen since the payload is signed..
    if (ENABLE_DEBUG) mail(DEBUG_EMAIL, 'Webhook Script Error', 'JSON decode failure');
    die('Invalid payload');
}


// Get the event ID
$eventID = $data['metadata']['eventID'];

// Insert the event into the database.

// IMPORTANT: To avoid duplicates, make sure that eventID is a primary key.

$stmt = $dbh->prepare("INSERT IGNORE INTO `events` (`eventID`, `received`, `data`) VALUES (:eventID, NOW(), :data)");

if (!$stmt)
{
    if (ENABLE_DEBUG) mail(DEBUG_EMAIL, 'Database error', var_export($dbh->errorInfo(), true));
}

if (!$stmt->execute([
    ":eventID" => $eventID,
    ":data" => $postData
]))
{
    if (ENABLE_DEBUG) mail(DEBUG_EMAIL, 'Database statement error', var_export($stmt->errorInfo(), true));
}


if ($stmt->rowCount() == 0)
{
    if (ENABLE_DEBUG) mail(DEBUG_EMAIL,'duplicate ans event', $postData);
    // Duplicate event
    die();
}
else
{
    if (!function_exists('fastcgi_finish_request'))
    {
        // If you're not using fastcgi, PLEASE don't keep the remote
        // server waiting while you do your event processing. Remember
        // you must respond within 5 seconds.

        // Run a script via a cron job to process your incoming events.

        die();
    }
    fastcgi_finish_request();


    // Process any unprocessed events.  You can do this here, or offload it into a cron job or something.

    $stmt = $dbh->query("SELECT `data`,`eventID` FROM `events` WHERE `processed` = 0 ORDER BY `received` ASC");
    while($row = $stmt->fetch(\PDO::FETCH_ASSOC))
    {
        // Do your processing

        // blah blah blah

        // All done? Mark the event as processed
        $substmt = $dbh->prepare("UPDATE `events` SET `processed` = 1 WHERE `eventID` = :id");
        $substmt->execute([":id" => $row['eventID']]);
    }
}