Webhook

Subscribe to webhooks to get updates on your transactions and payouts.



Create a webhook

From your Merchant Portal, go the the section Developers > Webhooks> Create



1. Setup your server


❗️

Webhook payload formats

Note that FlexFactor webhook payloads are not sent in application/json format. Please ensure that your endpoint can ingest these text-based messages. The payload is in proper JSON notation, so it can be serialized and parsed upon receipt.

Enter your webhook listener URL HTTPS endpoint, and accept POST requests from our Webhooks server.




📌

It is recommended to create one endpoint for all webhooks and not manage multiple endpoints

❗️

HTTPS protocol

Please note that the webhook sent from our system utilizes HTTPS protocol. If your server currently operates on HTTP the webhook transmission will not be successful.


2. Select the to event you want to subscribe to

Click on the event you want to subscribe to, then click Save


Order status

Result of FlexFactor action or decision on an order.

Event NamePriorityDescription
order.refundedCRITICALOccurs when the order was refunded.
order.completedRecommendedOccurs whenever customer approved the offer and FlexFactor created the order for processing.
order.cancelledRecommendedOccurs whenever an order is cancelled either by the system or by the merchant.
order.expiredRecommendedOccurs when an MIT transaction could not be rescued by FlexFactor by then end of the expiryDateUtc.
order.capturerequiredInformationalOccurs when the 'capture' flow is set for your configuration. This status will also be returned by the /evaluate api response.

Seepayload examples for critical and recommended webhooks.


Payment status

Events related to payments

Event NamePriorityDescription
payment.chargeback.receivedCRITICALTriggered when a chargeback occurs by the customer's issuing bank.
challenge.presentedCritical for challenge flowsTriggered when a challenge is presented to consumer.
challenge.attemptedCritical for challenge flowsTriggered when a challenge is attempted by consumer. It doesn't indicate success or failure.
challenge.passedCritical for challenge flowsTriggered when when consumer successfully completes a challenge.
challenge.failedCritical for challenge flowsTriggered when when consumer fails to complete a challenge.

Seepayload examples for critical webhooks.


Payout status

Events related to money transfer to your account

Event NamePriorityDescription
payout.createdInformationalA payout was created to send funds from FlexFactor account to the merchant's.
payout.updatedInformationalThe status of a payout was updated: pending, completed, failed, or reversed.

Application status

Events related to onboarding and application statuses. Relevant for Partner Integration.

Event NameDescription
application.submittedTriggered when an application was submitted and change from DRAFT to SUBMITTED status
application.canceledTriggered when an application was canceled and changed from any status to CANCELED
application.approvedTriggered when FlexFactor has approved a SUBMITTED application.
application.declinedTriggered when FlexFactor has declined a SUBMITTED application.
application.convertedTriggered when an application was converted and changed from APPROVED to CONVERTED status.




3. Webhook payloads

FlexFactor webhook server will send event objects to the endpoint URL set in step 1.

Fields description

Parse the event object to handle the incoming request accordingly by extracting the relevant information:

Field Name

Field Type

Description

Event

String

The name of the event that was triggered (in the case below, "order.completed")

TimeStamp

String

The timestamp of when the event occurred, in ISO 8601 format

ExternalOrderId

String

An ID representing the order in the external system

OrderId

Guid

A unique ID assigned by FlexFactor to identify the order

ConfirmationId

Guid

A unique ID assigned by FlexFactor to identify the confirmation of the order

IsTestMode

Boolean

A flag that indicates whether the event occurred in test mode or production mode

IsResent

Boolean

A flag that indicates whether the event was resent due to a failure


Payload examples

{
    "IdempotencyKey": "e4567890-d123-4abc-5678-4abcdef56789",
    "Event": "payment.chargeback.received",
    "TimeStamp": "2024-11-19T01:42:04.5149433Z",
    "EventData": {
        "DisputeDateTime": "2024-11-18T23:20:56Z",
        "Reason": "10.4 Dispute ref: 12345678-abcd-1234-efgh-567890abcdef",
        "TransactionId": "abcdef12-3456-7890-abcd-ef1234567890",
        "InitialTransactionReference": "00000000-0000-0000-0000-000000000000",
        "Amount": 4295,
        "Currency": "USD"
    },
    "Mid": "11223344-5566-7788-99aa-bbccddeeff00",
    "Pid": "99887766-5544-3322-1100-aabbccddeeff",
    "ExternalOrderId": "123456789012",
    "OrderId": "abcdef12-3456-7890-abcd-ef1234567890",
    "ConfirmationId": null,
    "IsTestMode": false,
    "IsResent": false
}
{
    "IdempotencyKey": "a1234567-b890-4cde-5678-5abcdef67890",
    "Event": "order.refunded",
    "TimeStamp": "2024-11-20T10:37:08.7405574Z",
    "EventData": {
        "Timestamp": "2024-11-20T10:37:08.6493879Z",
        "Message": "Rapid Dispute Resolution, refunding before it becomes a chargeback.",
        "TransactionId": "abcdef12-3456-7890-abcd-ef1234567890",
        "InitialTransactionReference": "00000000-0000-0000-0000-000000000000",
        "Amount": 8215,
        "Currency": "USD"
    },
    "Mid": "11223344-5566-7788-99aa-bbccddeeff00",
    "Pid": "99887766-5544-3322-1100-aabbccddeeff",
    "ExternalOrderId": "123456789012",
    "OrderId": "abcdef12-3456-7890-abcd-ef1234567890",
    "ConfirmationId": null,
    "IsTestMode": false,
    "IsResent": false
}
{
    "IdempotencyKey": "a2345678-b910-4def-8123-2abcdef34567",
    "Event": "order.completed",
    "TimeStamp": "2024-11-19T18:00:00.0000000Z",
    "EventData": null,
    "Mid": "11223344-5566-7788-99aa-bbccddeeff00",
    "Pid": "99887766-5544-3322-1100-aabbccddeeff",
    "ExternalOrderId": "123456789012",
    "OrderId": "abcdef12-3456-7890-abcd-ef1234567890",
    "ConfirmationId": "12345678",
    "IsTestMode": false,
    "IsResent": true
}
{
    "IdempotencyKey": "d1234567-e890-4bcd-91ef-1abcde345678",
    "Event": "order.cancelled",
    "TimeStamp": "2024-11-20T10:45:00.0000000Z",
    "EventData": null,
    "Mid": "11223344-5566-7788-99aa-bbccddeeff00",
    "Pid": "99887766-5544-3322-1100-aabbccddeeff",
    "ExternalOrderId": "123456789012",
    "OrderId": "abcdef12-3456-7890-abcd-ef1234567890",
    "ConfirmationId": null,
    "IsTestMode": false,
    "IsResent": true
}
{
    "IdempotencyKey": "b3456789-c012-4fab-9345-3abcdef45678",
    "Event": "order.expired",
    "TimeStamp": "2024-11-19T16:30:00.0000000Z",
    "EventData": null,
    "Mid": "11223344-5566-7788-99aa-bbccddeeff00",
    "Pid": "99887766-5544-3322-1100-aabbccddeeff",
    "ExternalOrderId": "123456789012",
    "OrderId": "abcdef12-3456-7890-abcd-ef1234567890",
    "ConfirmationId": null,
    "IsTestMode": false,
    "IsResent": true
}



Webhook validation

Time to response

Your endpoint must instantly return a successful status code (2xx) prior to any complex logic that could cause a timeout.

If FlexFactor doesn’t instantly receive a 2xx response status code for an event, we mark the event as failed and stop trying to send it to your endpoint. After multiple days, we email you about the misconfigured endpoint, and automatically disable it soon after if you haven’t addressed it.



FlexFactor signature

Every event FlexFactor sends to a webhook endpoint includes a signature generated through a SHA-512 hash-based message authentication code (HMAC).


To verify that the webhook is authentic and came from FlexFactor, your service needs to recreate the same hash using the same signing secret, and compare to the signature specified in the header x-fc-authorization header included in the webhook payload.

POST /webhook HTTP/1.1
Host: example.com
Content-Type: application/json
"x-fc-authorization": "HMAC-SHA512 SignedHeaders=x-fc-nonce;x-fc-date;host;x-fc-content-sha512&Signature=+HXN8ZewgINLk+uC/UI92HSWmLK7gZOECPxOGEM91ATyfyzScMF/+osEK5B0UjO7OFqahDvesSo8jmUWMZtQnA==",
"x-fc-content-sha512": "pLs0Op5VWqQM3ZIumqC2NP6MDqcnwFN1znp/oCuw9LcYd8PtvLC8ProyPg8ZDadsRc36NskT3QGKn/PkNqwWfg==",
"x-fc-date": "Mon, 20 Mar 2023 17:16:40 GMT",
"x-fc-nonce": "5f1c2de28a76457c9cb79d1740f2260a",
"x-fc-signature": "SbzcEwAKsViWqrB8+suZMjOdadswbUjLHtIKjDQJYle31xbB8Vr0pVTDaNP28/y+NDynpyFyKKnXmWZy8uJVig==",
"content-length": "255",

{"Event":"order.completed","TimeStamp":"2023-03-20T17:16:40.898703Z","EventData":null,"ExternalOrderId":"a9735210-1349-49bf-bfde-b737dd07872a","OrderId":"ac9674ed-cbfe-49aa-bc8b-eb1d2b74c429","ConfirmationId":"22ACD1D9","IsTestMode":false,"IsResent":false}

👍

Matching hashes

If the hashes match, it means that the webhook came from FlexFactor and has not been tampered with, and your service can process it.



Hashing functions and samples


exports.ComputeContentHash = function (payload) {
    var crypto = require('crypto');
    var hash = crypto.createHash('sha512');
    hash.write(payload);
    hash.end();
    return hash.read().toString('base64');

}

exports.ComputeSignature = function (secret, payload) {
    var crypto = require('crypto');
    var hash = crypto.createHash('sha512');

    var secretByteArray = Buffer.from(secret, 'base64');

    var hmac = crypto.createHmac('sha512', secretByteArray);
    hmac.write(payload);
    hmac.end();
    return hmac.read().toString('base64');

}

exports.ComputeWebhookSignature = function (secret, host,
                                            content, nonce, date) {
    var contentHash = exports.ComputeContentHash(content);
    var toSign = "POST\n" + nonce + ";" + date + ";" + host + ";" + contentHash;

    return exports.ComputeSignature(secret, toSign);
}


exports.Test = function () {
    // From FlexFactor Merchant Portal
    var subscriberKey = "XRmKBxG5uvt1qWzqvp+T6CAbTo0MB89GTxXZD5cHA56RP7Mj4NbnHQOR1Y8uorUU9YQz8ujaVRUdm9vTSkPZSw==";

    // Webhook received payload
    var payload = "{\"Event\":\"order.completed\",\"TimeStamp\":\"2023-03-20T17:16:40.898703Z\",\"EventData\":null,\"ExternalOrderId\":\"a9735210-1349-49bf-bfde-b737dd07872a\",\"OrderId\":\"ac9674ed-cbfe-49aa-bc8b-eb1d2b74c429\",\"ConfirmationId\":\"22ACD1D9\",\"IsTestMode\":true,\"IsResent\":false}";

    // Merchant's Webhook endpoint host
    var host = "your.endpoint.com";

    // From webhook's headers:

    //"x-fc-nonce" header
    var nonce = "5f1c2de28a76457c9cb79d1740f2260a";

    //"x-fc-date" header
    var date = "Mon, 20 Mar 2023 17:16:40 GMT";

    //from "x-fc-authorization" header
    var flexChargeAuthorization = "HMAC-SHA512 SignedHeaders=x-fc-nonce;x-fc-date;host;x-fc-content-sha512&Signature=+HXN8ZewgINLk+uC/UI92HSWmLK7gZOECPxOGEM91ATyfyzScMF/+osEK5B0UjO7OFqahDvesSo8jmUWMZtQnA==";


    // Extracting signature from x-fc-authorization header        
    var signature = flexChargeAuthorization.substr(flexChargeAuthorization.indexOf("Signature=") + 10);

    // Calculating signature
    var recalculatedSignature = exports.ComputeWebhookSignature(subscriberKey, host, payload, nonce, date);


    var verified = recalculatedSignature == signature;

    console.log("Signature verification result: " + verified);
}

exports.Test();
using System;
using System.Linq;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using FlexCharge.Webhooks.Services;

namespace FlexCharge.Webhooks.SignatureVerificationSample;

public static class Program
{
    public static void TestMain(string[] args)
    {
        // From FlexCharge Merchant Portal
        string subscriberKey = "XRmKBxG5uvt1qWzqvp+T6CAbTo0MB89GTxXZD5cHA56RP7Mj4NbnHQOR1Y8uorUU9YQz8ujaVRUdm9vTSkPZSw==";

        // Webhook received payload
        //var payload = message.Content.ReadAsStringAsync().Result;
        var payload = "{\"Event\":\"order.completed\",\"TimeStamp\":\"2023-03-20T17:16:40.898703Z\",\"EventData\":null,\"ExternalOrderId\":\"a9735210-1349-49bf-bfde-b737dd07872a\",\"OrderId\":\"ac9674ed-cbfe-49aa-bc8b-eb1d2b74c429\",\"ConfirmationId\":\"22ACD1D9\",\"IsTestMode\":true,\"IsResent\":false}";

        // Merchant's Webhook endpoint host
        string host = "fctestwebhook.free.beeceptor.com";

        // From webhook's headers:
        
        //var nonce = headers.GetValues("x-fc-nonce").First();
        var nonce = "5f1c2de28a76457c9cb79d1740f2260a";

        //var date = headers.GetValues("x-fc-date").First();
        var date = "Mon, 20 Mar 2023 17:16:40 GMT";

        //var var flexChargeAuthorization = headers.GetValues("x-fc-authorization").First();
        var flexChargeAuthorization = "HMAC-SHA512 SignedHeaders=x-fc-nonce;x-fc-date;host;x-fc-content-sha512&Signature=+HXN8ZewgINLk+uC/UI92HSWmLK7gZOECPxOGEM91ATyfyzScMF/+osEK5B0UjO7OFqahDvesSo8jmUWMZtQnA==";
        
        
        // Extracting signature from x-fc-authorization header        
        var signature = flexChargeAuthorization.Substring(flexChargeAuthorization.IndexOf("Signature=") + 10);

       
        // Calculating signature
        var recalculatedSignature = HMACSHA512.ComputeWebhookSignature(subscriberKey, host, payload, nonce, date);
        
        var verified = recalculatedSignature == signature;
        
        Console.WriteLine($"Signature verification result: {verified}");
    }
}

public class HMACSHA512
{
    public static string ComputeContentHash(string content, string hashAlgorithmName = "SHA512")
    {
        using (var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmName))
        {
            byte[] hashedBytes = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(content));
            return Convert.ToBase64String(hashedBytes);
        }
    }

    public static string ComputeSignature(string stringToSign, string secret)
    {
        using (var hmacsha512 = new System.Security.Cryptography.HMACSHA512(Convert.FromBase64String(secret)))
        {
            var bytes = Encoding.UTF8.GetBytes(stringToSign);
            var hashedBytes = hmacsha512.ComputeHash(bytes);
            return Convert.ToBase64String(hashedBytes);
        }
    }

    public static string ComputeWebhookSignature(string subscriberKey, string host, 
        string payload, string nonce, string date)
    {
        var contentHash = ComputeContentHash(payload);
        var toSign = $"POST\n{nonce};{date};{host};{contentHash}";

        return ComputeSignature(toSign, subscriberKey);
    }
    
    public static bool VerifyWebhookSignature(Uri webhookEndpoint, HttpHeaders headers, 
        string payload, string subscriberKey)
    {
        var nonce = headers.GetValues("x-fc-nonce").First();
        var date = headers.GetValues("x-fc-date").First();
        var flexChargeAuthorization = headers.GetValues("x-fc-authorization").First();
        var signature = flexChargeAuthorization.Substring(flexChargeAuthorization.IndexOf("Signature=") + 10);
        
        var recalculatedSignature = HMACSHA512.ComputeWebhookSignature(subscriberKey, webhookEndpoint.Host, payload, nonce, date);

        return recalculatedSignature == signature;
    }
}

❗️

Non matching hashes

If the hashes do not match, it means that the webhook payload has been modified or was not sent by FlexFactor, and your service should discard the webhook to prevent any potential security issues.