Webhooks: Difference between revisions

From CasperTech Wiki
Jump to navigation Jump to search
No edit summary
Line 1: Line 1:
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.
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 server(s) under your control.


= <span style="color:#00528c">Benefits</span> =
= <span style="color:#00528c">Benefits</span> =

Revision as of 04:02, 20 February 2018

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 server(s) under your control.

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 3 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

This code assumes that you have a database created using the SQL above.

<?php

// Configuration options

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

define('ENABLE_DEBUG', true);
define('DEBUG_EMAIL', 'your@email.address');

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 3 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']]);
    }
}