Webhooks: Difference between revisions
Jump to navigation
Jump to search
(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...") |
(→PHP) |
||
Line 36: | Line 36: | ||
== PHP == | == PHP == | ||
This code assumes that you have a database created using the SQL above. | |||
<syntaxhighlight lang="php" line='line'> | <syntaxhighlight lang="php" line='line'> |
Revision as of 03:35, 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 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
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', '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']]);
}
}