Webhooks are a way for a merchant to receive Signifyd decisions asynchronously.

Signifyd will push the notification to the merchant, allowing them to continue processing based on the decision contained within.

Webhook Events

You can create webhooks in Signifyd for the following events. Each event has a corresponding topic identifier
which will be sent in the SIGNIFYD-TOPIC header of the webhook.

Currently, the following events can trigger a webhook. Only one URL may be specified per event.

EventSIGNIFYD-TOPICDescriptionResponse
Merchant ReviewMERCHANT_REVIEWSent any time a user assigns a case a Review Disposition (thumbs up/down on console)View
Signifyd ReviewSIGNIFYD_REVIEWSent any time a decision is made by Signifyd agent on a caseView
CheckoutCHECKOUTSent any time a decision is made in response to a checkout eventView
SaleSALESent any time a decision is made in response to a sale eventView
TransactionTRANSACTIONSent any time a decision is made in response to a transaction eventView
RerouteREROUTESent any time a decision is made in response to a reroute eventView

Webhook Verification

To allow a client to verify a webhook message has in fact come from Signifyd, an SIGNIFYD-SEC-HMAC-SHA256 header is included in each webhook POST message. The contents of this header is the Base64 encoded output of the HMAC SHA256 encoding of the JSON body of the message, using the team's API key as the encryption key. Here's an example of how to compute the Json body as an HMAC encoded value in Java.

String teamApiKey = "testKey";
String hmacHeaderKey = "X-SIGNIFYD-SEC-HMAC-SHA256";
String webhookBody = request.body().asText();

// Hash and encode the webhookBody
Mac sha256HMAC = javax.crypto.Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(teamApiKey.getBytes(), "HmacSHA256");
sha256HMAC.init(secretKey);
String checkHash = Base64.encodeBase64String(sha256HMAC.doFinal(webhookBody.getBytes(StandardCharsets.UTF_8)));

// Compare the original hmacHeader with the new checkHash
boolean isValidRequest = request.getHeaders()
                                .get(hmacHeaderKey)
                                .map(hmacHeader -> hmacHeader.equals(checkHash))
                                .orElse(false);
const teamApiKey = "testKey";
const hmacHeaderKey = "X-SIGNIFYD-SEC-HMAC-SHA256";
// Note: Avoid mapping the request body to an object prior to hmac validation
const webhookBody = req.body.toString('utf8');

// Hash and encode the webhookBody
const crypto = require('crypto');
const checkHash = crypto.createHmac('sha256', teamApiKey)
												.update(webhookBody, 'utf8')
												.digest('base64');

// Compare the original hmacHeader with the new checkHash
const hmacHeader = req.header(hmacHeaderKey);
const isValidRequest = (typeof hmacHeader !== "undefined") 
                            ? hmacHeader == checkHash 
                            : false;
$teamApiKey = 'testKey';
/* Note: Signifyd's hmac header 'X-SIGNIFYD-SEC-HMAC-SHA256'
   but PHP changes this to 'HTTP_X_SIGNIFYD_SEC_HMAC_SHA256' on $_SERVER */
$hmacHeaderKey = 'HTTP_' . str_replace('-', '_', 'X-SIGNIFYD-SEC-HMAC-SHA256');
$webhookBody = file_get_contents("php://input");

// Hash and encode the webhookBody
$checkHash = base64_encode(hash_hmac('sha256', $webhookBody, $teamApiKey, true));

// Compare the original hmacHeader with the new checkHash
$isValidRequest = isset($_SERVER[$hmacHeaderKey]) 
                        ? ($_SERVER[$hmacHeaderKey] == $checkHash ? true : false) 
                        : false;

Webhook Response

{
  "signifydId": 44,
  "orderId": "XGR-1840823423",
  "decision": {
    "createdAt": "2020-11-20T20:16:15.382889Z",
    "checkpointAction": "ACCEPT",
    "checkpointActionReason": "Power buyer on approve list",
    "checkpointActionPolicy": "APPROVE_POWER_BUYERS",
    "policies": {
      "default": {
        "name": "SIGNIFYD_DECISION",
        "status": "EVALUATED_TRUE",
        "action": "REJECT",
        "reason": "Suspicious user profile"
      },
      "overriding": [
        {
          "name": "APPROVE_COOL_BUYERS",
          "status": "EVALUATED_TRUE",
          "action": "ACCEPT",
          "reason": "Buyer was cool"
        }
      ]
    },
    "score": 0
  },
  "coverage": {
    "fraudChargebacks": {
      "amount": 105.99,
      "currency": "GBP"
    },
    "inrChargebacks": {
      "amount": 105.99,
      "currency": "GBP"
    },
    "allChargebacks": {
      "amount": 105.99,
      "currency": "GBP"
    }
  }
}