Documentation You are here: start » v » 2.0 » webhooks

Webhooks

Overview

Foxy's Webhook functionality allows you to create integrations which subscribe to notifications of new transactions from your Foxy store. When a new transaction is successfully completed, we send a HTTP POST payload to each of your configured endpoints with information about the completed transaction. Your endpoint can then perform any custom tasks you need, such as updating inventory levels, passing information on to fulfilment services, subscribing customers to mailing lists.

We currently have four different types of Webhook endpoints you can utilise, depending on your needs:

Sending Webhooks & Automatic Retries

When a new transaction is placed and your webhooks are triggered, Foxy will wait for 1 minute for a response from your webhook endpoint. If a webhook fails to respond successfully (that is, if we don't receive a HTTP response code of 200, or in the case of the Legacy XML Webhook a response body of just foxy) in that time for any reason, Foxy will automatically reattempt to connect to your endpoint 11 more times within the following hour after it was initially attempted. The retries will get progressively further apart over the hour: ranging from around 1 minute apart up to around 10 minutes apart. If any of the retries are successful, no further attempts will be made.

PayPal, Amazon Pay, and other Hosted Gateways

Using a hosted gateway? Multiple notifications may be sent per transaction.

If you use any of the hosted gateways, note that every transaction may generate more than one notification. Every time our IPN (Instant Payment Notification) server gets a notification from the gateway we also send you a notification. Generally, these notifications should arrive to your webhook endpoint in order, but it's possible that these webhooks are sent to your endpoint within a very short window (less than 0.5 seconds), and with possible network delays, your endpoint may receive the notifications out of order. Make sure you account for the status value in your logic. For example, you likely don't want to ship products until the status has gone from pending to approved.

The webhook will include a status field which is updated with each notification. Valid status entries include approved, authorized, declined, pending, and rejected. (This node may also be empty if the payment was made by a payment method for which Foxy doesn't currently support statuses.)

For instance, depending on your account settings BitPay server may send up to 4 notifications for every transaction. Other hosted gateways usually send 2 notifications: when the transaction is initiated and when it is complete.

Here is the current list of hosted gateways which take advantage of the status field:

  • 2Checkout
  • Adyen
  • Amazon
  • BitPay
  • Coinbase
  • Comgate
  • Cybersource Secure Acceptance Web/Mobile
  • DIBS
  • Dwolla
  • MercadoPago
  • Mollie
  • Ogone
  • PayPal Express Checkout
  • PayPal Plus
  • PesaPal
  • Skrill

Handling Failed Webhooks

If after all retries there is at least one webhook that was unsuccessful, an entry will be added to the stores error log with details about each webhook that didn't succeed. The error information will be made up of the following parts:

TITLE (TYPE): ERROR_MESSAGE [HTTP_CODE]
TITLE
The title of the webhook
TYPE
The type of webhook - json, legacy_xml, zapier, webflow.
ERROR_MESSAGE
The error message received from the endpoint, trimmed to 500 characters. For the JSON Webhook this is the body of the response.
HTTP_CODE
The HTTP status code of the response

Along with the error log, an email will also be sent to the store's configured email address(es) with some general information about the error.

Did the transaction complete?

The transaction that triggered the webhook still completed successfully, the customer was charged and you'll receive payment for the transaction as your chosen payment gateway processes it.

Troubleshooting Webhook Errors

If at least one of your webhooks failed to handle the payload successfully - you will want to correct the error and refeed the payload to the failed endpoints.

Check the error: You can see the specific error that was triggered by your webhook by viewing the errors page of your stores FoxyCart administration. Check out this page: http://wiki.foxycart.com/primer/errors for information on what the error logs mean for your webhook. Based on the particular error you're receiving, you may need to make changes to your webhook to correct the issue. If you don't manage the specific webhook that has errored, you'll need to get in touch with the people that do with details of the error.

Retrying Webhooks

If you want to trigger a transaction to be resent to your webhook, you can do that from your store's FoxyCart administration by following these steps:

  1. Load the transactions report section of the administration.
  2. Find the transaction that you want to retry - if it has previously failed to send to your webhooks successfully, it will be highlighted in red - and expand that transaction by clicking the “[+]” link.
  3. At the bottom of the transaction's details, look for the “Webhooks” section.
  4. Check the checkbox next to any webhooks that you want to send, and press the “Refeed selected webhooks” button.

After triggering the transaction to be sent to your webhook again, the system will re-attempt the process again for another hour as detailed above - or until it is successful.

JSON Webhook

The JSON webhook is, as you may have guessed, a JSON formatted payload sent to your endpoint.

Creating a webhook

JSON webhooks can be created from the “Integrations” section of the FoxyCart administration. Enable the “JSON Webhooks” checkbox, and you'll be able to specify a new webhook endpoint.

Each webhook requires a name, a URL and an encryption key (which will be automatically set with a value). Enter an identifying name and the URL to the endpoint that will be receiving the webhook payload. We'll explain how to create that endpoint in the next section.

If you'd like to provide your own encryption key for the webhook, click the “Show secret” button and enter in your own secure key there.

You can add multiple endpoints by clicking the “Add one more URL” button within the JSON webhooks section.

Once you've added in your new webhook - click the “Update Webhooks” button to save.

Webhook Events

JSON webhooks can be triggered for different events that occur in your store, selected when creating or editing the webhook. Currently we support two events, with more to come in the future.

Transaction Created

The Transaction Created webhook option receives any transaction for the store, whether it's a new transaction, a subscription renewal, an update of customer info, or a customer-initiated subscription cancel (technically, when a subscription end date is set – always a future date). Those all generate a transaction in the system, so are fed to the endpoint.

Subscription Cancelled

The Subscription Cancelled event occurs when a subscription is actually ended, on the end date set on the subscription.

Subscriptions that are made inactive directly via the administration or the API do not trigger this event. Customers completing a sub_cancel checkout type also don't trigger this event, they trigger a Transaction Created webhook instead, with a transaction_type attribute in the payload of subscription_cancellation

Testing Webhooks: Helpful Tips

It's often helpful to see exactly what the webhook request (sent to your servers) looks like. Though we don't offer this functionality natively, there are a variety of free services that you can use to send and view webhook requests. One we like is Webhooks.site. You can get an endpoint there, configure Foxy to send webhooks to that URI, then review the full request (body and headers). There are others like Hookbin, or Requestb.in (which you can set up to self-host if desired, like webhook.site).

Receiving a webhook

Once you've configured your webhook - whenever actions happen that relate to the events you're subscribed to, a JSON payload containing information about that event will be sent to your endpoint. If your endpoint is secured with HTTPS, then the data is sent unencrypted ready for you to use. If your endpoint is not secured though, the data will be encrypted using AES-256-CBC before being sent to your endpoint, meaning you will need to then decrypt it before you can use it.

Headers

Any requests made to your webhook's endpoint will also contain several special headers:

Header Description
Foxy-Webhook-Event Name of the event that triggered this payload. Currently transaction/created and subscription/cancelled.
Foxy-Webhook-Signature A HMAC SHA256 signature of the payload, using the webhook's encryption key. Used for verifying the contents of the payload.
Foxy-Webhook-Refeed A boolean to signify if this payload has been refed. If false, then this is the first time this specific instance of the event has been triggered.
Foxy-Store-ID The ID of the store that this webhook was triggered for.
Foxy-Store-Domain The current Foxy store domain that this webhook was triggered for.

Example Payload

The JSON payload will follow much the same structure as our Hypermedia API. Most of the objects will include the API's _links array. These contain helpful URI's that could be used to access the specific resources in the API if you're also making use of that. If not, these can be safely ignored.

The source of the payload data is our Hypermedia API, so you can use the documentation for the different resources for further information on the data it contains.

In the example payloads below - to save space, the _links arrays have been cleared out.

Transaction Created

{
    "_embedded": {
        "fx:applied_taxes": [
            {
                "_links": { },
                "amount": 2.24,
                "apply_to_handling": true,
                "apply_to_shipping": true,
                "date_created": null,
                "date_modified": null,
                "is_future_tax": false,
                "name": "Global Tax",
                "rate": 5,
                "shipto": null
            }
        ],
        "fx:billing_addresses": [
            {
                "_links": { },
                "address1": "123 Test Street",
                "address2": "Level 2",
                "address_name": "Default Billing Address",
                "city": "SAINT PAUL",
                "company": "Some Company",
                "customer_country": "US",
                "customer_phone": "123456789",
                "customer_postal_code": "55115",
                "date_created": null,
                "date_modified": "2018-03-01T00:00:00-0700",
                "first_name": "John",
                "last_name": "Smith",
                "region": "MN"
            }
        ],
        "fx:custom_fields": [
            {
                "_links": { },
                "date_created": null,
                "date_modified": null,
                "is_hidden": true,
                "name": "custom_attribute",
                "value": "reseller"
            },
            {
                "_links": { },
                "date_created": null,
                "date_modified": null,
                "is_hidden": false,
                "name": "newsletter_subscribe",
                "value": "1"
            }
        ],
        "fx:customer": {
            "_links": { },
            "date_created": null,
            "date_modified": null,
            "email": "john@example.com",
            "first_name": "John",
            "forgot_password": null,
            "forgot_password_timestamp": null,
            "id": 12345,
            "is_anonymous": false,
            "last_login_date": "2018-03-01T00:00:00-0700",
            "last_name": "Smith",
            "password_hash": "$2y$13$Jx7p31h18VnW.ab38QaYJu1QCzsyMQZKdY5s21nYewmGQj0OQCyIq",
            "password_hash_config": "13",
            "password_hash_type": "bcrypt",
            "password_salt": null,
            "tax_id": null
        },
        "fx:discounts": [
            {
                "_links": { },
                "amount": -3.79,
                "code": "my-code",
                "date_created": null,
                "date_modified": null,
                "display": "-$3.79",
                "is_future_discount": false,
                "is_taxable": false,
                "name": "Special Offer"
            }
        ],
        "fx:items": [
            {
                "_embedded": {
                    "fx:item_category": {
                        "_links": { },
                        "admin_email": null,
                        "admin_email_template_uri": null,
                        "code": "DEFAULT",
                        "customer_email_template_uri": null,
                        "customs_value": 0,
                        "date_created": null,
                        "date_modified": null,
                        "default_length_unit": "IN",
                        "default_weight": 0,
                        "default_weight_unit": "LBS",
                        "discount_details": "single|3-5|5-10",
                        "discount_name": "Bulk Order",
                        "discount_type": "quantity_percentage",
                        "handling_fee": 1,
                        "handling_fee_minimum": 0,
                        "handling_fee_percentage": 80,
                        "handling_fee_type": "none",
                        "item_delivery_type": "flat_rate",
                        "max_downloads_per_customer": 3,
                        "max_downloads_time_period": 24,
                        "name": "Default for all products",
                        "send_admin_email": false,
                        "send_customer_email": false,
                        "shipping_flat_rate": 2,
                        "shipping_flat_rate_type": "per_item"
                    },
                    "fx:item_options": [
                        {
                            "_links": { },
                            "date_created": null,
                            "date_modified": null,
                            "name": "size",
                            "price_mod": 0,
                            "value": "Large",
                            "weight_mod": 0
                        },
                        {
                            "_links": { },
                            "date_created": null,
                            "date_modified": null,
                            "name": "color",
                            "price_mod": 0,
                            "value": "Blue",
                            "weight_mod": 0
                        },
                        {
                            "_links": { },
                            "date_created": null,
                            "date_modified": null,
                            "name": "Bulk Order",
                            "price_mod": -0.11,
                            "value": "-2%",
                            "weight_mod": 0
                        }
                    ]
                },
                "_links": { },
                "base_price": 5.5,
                "code": "my-widget",
                "date_created": null,
                "date_modified": "2018-03-01T00:00:00-0700",
                "delivery_type": "flat_rate",
                "discount_details": null,
                "discount_name": null,
                "discount_type": null,
                "downloadable_url": null,
                "expires": 0,
                "height": 0,
                "image": null,
                "is_future_line_item": false,
                "item_category_uri": "https://api.foxycart.com/item_categories/100",
                "length": 0,
                "name": "My Widget",
                "parent_code": null,
                "price": 5.5,
                "quantity": 5,
                "quantity_max": 0,
                "quantity_min": 0,
                "shipto": null,
                "sub_token_url": null,
                "subscription_end_date": null,
                "subscription_frequency": null,
                "subscription_next_transaction_date": null,
                "subscription_start_date": null,
                "url": null,
                "weight": 0,
                "width": 0
            },
            {
                "_embedded": {
                    "fx:item_category": {
                        "_links": { },
                        "admin_email": null,
                        "admin_email_template_uri": null,
                        "code": "subs",
                        "customer_email_template_uri": null,
                        "customs_value": 0,
                        "date_created": null,
                        "date_modified": null,
                        "default_length_unit": "IN",
                        "default_weight": 1,
                        "default_weight_unit": "LBS",
                        "discount_details": "",
                        "discount_name": "",
                        "discount_type": null,
                        "handling_fee": 0,
                        "handling_fee_minimum": 0,
                        "handling_fee_percentage": 0,
                        "handling_fee_type": "none",
                        "item_delivery_type": "shipped",
                        "max_downloads_per_customer": 3,
                        "max_downloads_time_period": 24,
                        "name": "Subscriptions",
                        "send_admin_email": false,
                        "send_customer_email": false,
                        "shipping_flat_rate": 0,
                        "shipping_flat_rate_type": "per_order"
                    },
                    "fx:item_options": [
                        {
                            "_links": { },
                            "date_created": null,
                            "date_modified": null,
                            "name": "size",
                            "price_mod": 0,
                            "value": "Large",
                            "weight_mod": 0
                        }
                    ]
                },
                "_links": { },
                "base_price": 10.95,
                "code": "widget-sub",
                "date_created": null,
                "date_modified": "2018-03-01T00:00:00-0700",
                "delivery_type": "shipped",
                "discount_details": null,
                "discount_name": null,
                "discount_type": null,
                "downloadable_url": null,
                "expires": 0,
                "height": 0,
                "image": null,
                "is_future_line_item": false,
                "item_category_uri": "https://api.foxycart.com/item_categories/101",
                "length": 0,
                "name": "Widget Subscription",
                "parent_code": null,
                "price": 10.95,
                "quantity": 1,
                "quantity_max": 0,
                "quantity_min": 0,
                "shipto": null,
                "sub_token_url": "https://example.foxycart.com/cart?sub_token=127c8410bce9896f0d4a0950a3cf3796514e3fd2a8aa55476c3d0c5de184dbc7",
                "subscription_end_date": null,
                "subscription_frequency": "1m",
                "subscription_next_transaction_date": "2018-04-01T00:00:00-0700",
                "subscription_start_date": "2018-03-01T00:00:00-0700",
                "url": null,
                "weight": 1,
                "width": 0
            }
        ],
        "fx:payments": [
            {
                "_links": { },
                "amount": 54.2,
                "cc_exp_month": "05",
                "cc_exp_year": "2025",
                "cc_number_masked": "xxxxxxxxxxxx4242",
                "cc_type": "Visa",
                "date_created": "2018-03-01T00:00:00-0700",
                "date_modified": "2018-03-01T00:00:00-0700",
                "fraud_protection_score": 0,
                "gateway_type": "vantiv",
                "paypal_payer_id": null,
                "processor_response": "Vantiv Transaction ID: 123456789123456789",
                "processor_response_details": null,
                "purchase_order": null,
                "third_party_id": null,
                "type": "plastic"
            }
        ],
        "fx:shipments": [
            {
                "_links": { },
                "address1": "123 Test Street",
                "address2": "Level 2",
                "address_name": "Me",
                "city": "SAINT PAUL",
                "company": "Some Company",
                "country": "US",
                "date_created": null,
                "date_modified": "2018-03-01T00:00:00-0700",
                "first_name": "John",
                "last_name": "Smith",
                "phone": "123456789",
                "postal_code": "55115",
                "region": "MN",
                "shipping_service_description": "USPS Priority Mail 2-Day",
                "shipping_service_id": 14,
                "total_item_price": 37.9,
                "total_price": 54.2,
                "total_shipping": 17.85,
                "total_tax": 2.24
            }
        ]
    },
    "_links": { },
    "currency": "USD",
    "customer_email": "john@example.com",
    "customer_first_name": "John",
    "customer_ip": "193.123.4.56",
    "customer_last_name": "Smith",
    "customer_tax_id": null,
    "data_is_fed": true,
    "date_created": null,
    "date_modified": "2018-03-01T00:00:00-0700",
    "display_id": 3456789,
    "hide_transaction": false,
    "id": 3456789,
    "ip_country": "United States",
    "is_new_subscription": true,
    "is_test": true,
    "locale_code": "en_US",
    "receipt_url": "https://example.foxycart.com/receipt?id=2a5175756293151099587e3b45590376",
    "status": "approved",
    "store_id": "12345",
    "store_version": "2.0",
    "total_discount": -3.79,
    "total_future_item_price": 0,
    "total_future_shipping": 0,
    "total_item_price": 37.9,
    "total_order": 54.2,
    "total_shipping": 17.85,
    "total_tax": 2.24,
    "transaction_date": "2018-03-01T00:00:00-0700",
    "transaction_type": "transaction"
}

There is also a few extra attributes included to help identify information about the transaction:

transaction_type
What type of transaction this was, could be transaction, updateinfo, subscription_modification, subscription_renewal or subscription_cancellation
is_new_subscription
A boolean value representing if a new subscription is included in this transaction or not.

Subscription Cancelled

{
    "_embedded": {
        "fx:customer": {
            "_links": { },
            "date_created": null,
            "date_modified": "2019-08-09T23:44:00-0700",
            "email": "john@example.com",
            "first_name": "John",
            "forgot_password": null,
            "forgot_password_timestamp": null,
            "id": 12345,
            "is_anonymous": false,
            "last_login_date": "2019-08-09T23:43:49-0700",
            "last_name": "Smith",
            "password_hash": "$2y$13$Jx7p31h18VnW.ab38QaYJu1QCzsyMQZKdY5s21nYewmGQj0OQCyIq",
            "password_hash_config": "13",
            "password_hash_type": "bcrypt",
            "password_salt": null,
            "tax_id": null
        },
        "fx:transaction_template": {
            "_embedded": {
                "fx:discounts": [
                    {
                        "_links": { },
                        "amount": -2,
                        "code": "coupon",
                        "date_created": null,
                        "date_modified": null,
                        "display": "-$2.00",
                        "is_future_discount": false,
                        "is_taxable": false,
                        "name": "Default Discount"
                    }
                ],
                "fx:items": [
                    {
                        "_embedded": {
                            "fx:item_category": {
                                "_links": { },
                                "admin_email": "john@example.com",
                                "admin_email_template_uri": null,
                                "code": "DEFAULT",
                                "customer_email_template_uri": null,
                                "customs_value": 0,
                                "date_created": null,
                                "date_modified": "2019-08-09T23:47:58-0700",
                                "default_length_unit": "IN",
                                "default_weight": 0,
                                "default_weight_unit": "LBS",
                                "discount_details": "1-10",
                                "discount_name": "Gift Set",
                                "discount_type": null,
                                "handling_fee": 1,
                                "handling_fee_minimum": 0,
                                "handling_fee_percentage": 10,
                                "handling_fee_type": "flat_percent_with_minimum",
                                "item_delivery_type": "flat_rate",
                                "max_downloads_per_customer": 3,
                                "max_downloads_time_period": 24,
                                "name": "Default for all products",
                                "send_admin_email": false,
                                "send_customer_email": false,
                                "shipping_flat_rate": 5,
                                "shipping_flat_rate_type": "per_order"
                            }
                        },
                        "_links": { },
                        "code": null,
                        "date_created": null,
                        "date_modified": "2019-08-10T00:40:14-0700",
                        "discount_details": null,
                        "discount_name": null,
                        "discount_type": null,
                        "expires": 0,
                        "height": 0,
                        "image": null,
                        "is_future_line_item": false,
                        "item_category_uri": "https://api.foxycart.com/item_categories/100",
                        "length": 0,
                        "name": "test",
                        "parent_code": null,
                        "price": 20,
                        "quantity": 1,
                        "quantity_max": 0,
                        "quantity_min": 0,
                        "shipto": "Me",
                        "subscription_end_date": "2019-08-11T00:00:00-0700",
                        "subscription_frequency": "1y",
                        "subscription_next_transaction_date": "2019-10-24T00:00:00-0700",
                        "subscription_start_date": "2017-10-24T00:00:00-0700",
                        "url": null,
                        "weight": 0,
                        "width": 0
                    }
                ]
            },
            "_links": { },
            "billing_address1": "123 Test Street",
            "billing_address2": null,
            "billing_city": "SAINT PAUL",
            "billing_company": "Some Company",
            "billing_country": "US",
            "billing_first_name": "John",
            "billing_last_name": "Smith",
            "billing_phone": null,
            "billing_postal_code": "5000",
            "billing_state": "MN",
            "customer_uri": "https://api.foxycart.com/customers/15460475",
            "date_created": null,
            "date_modified": "2019-08-10T00:40:14-0700",
            "language": null,
            "locale_code": "en_US",
            "shipping_address1": "123 Test Street",
            "shipping_address2": null,
            "shipping_city": "SAINT PAUL",
            "shipping_company": "Some Company",
            "shipping_country": "US",
            "shipping_first_name": "John",
            "shipping_last_name": "Smith",
            "shipping_phone": null,
            "shipping_postal_code": "5000",
            "shipping_state": "MN",
            "template_set_uri": null,
            "total_future_shipping": 0,
            "total_item_price": 20,
            "total_order": 25,
            "total_shipping": 5,
            "total_tax": 0,
            "use_customer_shipping_address": false
        }
    },
    "_links": { },
    "date_created": null,
    "date_modified": "2019-08-11T04:13:01-0700",
    "end_date": "2019-08-11T00:00:00-0700",
    "error_message": null,
    "first_failed_transaction_date": null,
    "frequency": "1y",
    "is_active": false,
    "next_transaction_date": "2019-10-24T00:00:00-0700",
    "past_due_amount": 0,
    "start_date": "2017-10-24T00:00:00-0700",
    "sub_token_url": "https://example.foxycart.com/cart?sub_token=b37fb078e12ba5ef86773b43e60bdea5124a46f4b5698a5387862cd82042d003c",
    "third_party_id": null
}

Validating the payload

Using the encryption key associated with each webhook, a signature is created of the payload that is passed with each request as a header, specifically as Foxy-Webhook-Signature. This signature can then be used on your endpoint to confirm that the request has come from a trusted source, in this case from Foxy.

To create a signature on your endpoint to compare, you generate a HMAC SHA256 hash of the data you've received, using the encryption key for the webhook as the key. The resulting signature should be in a hexadecimal format, and you would then perform a secure comparison of the two values to verify that the data is as it should be.

Example Endpoints

The following are some examples of code you can use for your Webhook endpoints, which include logic to validate the payload against the header signature value. The code assumes that your endpoint is secured with an SSL certificate - if you have an insecure endpoint, you'll need to review the next section on Handling Encrypted Payloads as well.

PHP

<?php
 
define('FOXY_WEBHOOK_ENCRYPTION_KEY', 'ABC123');
 
$data = file_get_contents('php://input');
$parsedData = json_decode($data, true);
$event = $_SERVER['HTTP_FOXY_WEBHOOK_EVENT'];
 
// Verify the webhook payload
$signature = hash_hmac('sha256', $data, FOXY_WEBHOOK_ENCRYPTION_KEY);
if (!hash_equals($signature, $_SERVER['HTTP_FOXY_WEBHOOK_SIGNATURE'])) {
    echo "Signature verification failed - data corrupted";
    http_response_code(500);
    return;
}
 
if (is_array($parsedData)) {
    // Handle the payload
 
    if ($event == "transaction/created") {
        // The following is an example of working with the transaction/created payload
        $email_address = $parsedData['customer_email'];
        $billing_country = $parsedData['_embedded']['fx:billing_addresses']['country'];
        $shipping_country = $parsedData['_embedded']['fx:shipments'][0]['country']; // Assuming single-ship
 
        $has_small_product_a = false;
        $has_large_product_a = false;
 
        foreach ($parsedData['_embedded']['fx:items'] as $item) {
            $name = $item['name'];
            $quantity = $item['quantity'];
            $category = $item['_embedded']['fx:item_category']['code'];
 
            if ($item['name'] == "Product A") {
                foreach($item['_embedded']['fx:item_options'] as $item_option) {
                    if ($item_option['name'] == "size") {
                        if ($item_option['value'] == "small") {
                            $has_small_product_a = true;
                        } else if ($item_option['value'] == "large") {
                            $has_large_product_a = true;
                        }
                    }
                }
            }
        }
    }
 
} else {
    // JSON data not found
    echo("No data");
    http_response_code(500);
    return;
}

Ruby

This simple Ruby example is using a small Sinatra app - some logic may need to be adjusted if you're using a different framework.

require 'sinatra'
require 'json'
 
def verify_webhook(data, encryption_key)
    signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), encryption_key, data)
    return halt 500, "Signature verification failed - data corrupted" unless Rack::Utils.secure_compare(signature, request.env['HTTP_FOXY_WEBHOOK_SIGNATURE'])
end
 
post '/webhook' do
    request.body.rewind
    data = request.body.read
    event = request.env['HTTP_FOXY_WEBHOOK_EVENT']
    verify_webhook(data, ENV['FOXY_WEBHOOK_ENCRYPTION_KEY'])
 
    parsedData = JSON.parse(data)
 
    # Handle the payload
    puts parsedData['id']
end

Important Notes

  • You should avoid storing your Webhook encryption key directly within your codebase. Instead, store it as an environment variable for your application
  • When comparing your locally created hmac signature to the one from the request header, avoid doing a direct equality comparison like ==. Instead, use a more secure method of comparison that comes with your coding language of choice, such as secure_compare() in Ruby and hash_equals() in PHP 5.6+. If you don't have access to PHP 5.6+, you can use one of the user supplied polyfill functions on this page.

Handling encrypted payloads

While we strongly recommend setting a webhook endpoint that is secured with an SSL certificate - we do also support insecure endpoint URL's as well. If you're using an insecure endpoint URL, instead of being passed the raw payload - it will be sent encrypted using AES-256-CBC, and as such will need to be decrypted on your endpoint before you can work with it.

The payload is sent with three hexadecimal values representing the MAC (Message Authentication Code), IV (Initialization Vector) and the encrypted data, concatenated by a : character. To decrypt the payload, you can follow these steps:

  1. Split the payload into it's three pieces by the : character
  2. Confirm the validity of the payload by completing a secure comparison of the MAC value to a HMAC SHA256 digest of the IV and data values concatenated together.
  3. Convert the IV value from hexadecimal to binary
  4. Create a binary SHA256 hash of the webhook encryption key
  5. Decrypt the data as AES-256-CBC using the IV and key binary values you just created
  6. Parse the decrypted data as JSON

The following is some example implementations of performing those steps:

PHP

<?php
 
$parts = explode(':', $data);
$mac = $parts[0];
$iv = $parts[1];
$data = $parts[2];
 
$calc_mac = hash('sha256', "$iv:$data");
 
if (hash_equals($calc_mac, $mac)) {
    $iv = hex2bin($iv);
    $key = hex2bin(hash('sha256', FOXY_WEBHOOK_ENCRYPTION_KEY));
 
    if ($data = openssl_decrypt($data, 'aes-256-cbc', $key, 0, $iv)) {
        $parsedData = json_decode($data, true);
    } else {
        while ($msg = openssl_error_string()) {
            echo("Openssl error: " . $msg);
        }
        http_response_code(500);
        return;
    }
} else {
    // Encrypted data corrupted
    echo("Encrypted data corrupted");
    http_response_code(500);
    return;
}

Ruby

mac, iv, data = data.split(":")
calc_mac = Digest::SHA256.hexdigest("#{iv}:#{data}")
 
if Rack::Utils.secure_compare(calc_mac, mac)
    begin
        data = Base64::decode64(data)
        decipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
        decipher.decrypt
        decipher.key = Digest::SHA256.digest(ENV['FOXY_WEBHOOK_ENCRYPTION_KEY'])
        decipher.iv = iv.scan(/../).map { |x| x.hex.chr }.join
        data = decipher.update(data) + decipher.final
    rescue OpenSSL::Cipher::CipherError
        halt(500, "Unable to decrpyt data")
    end
else
    halt(500, "Encrypted data corrupted")
end

Legacy XML Webhook

The Legacy XML Webhook matches our previous XML datafeed functionality, and shouldn't be used for any brand new integrations - but can be useful for connecting to existing endpoints/integrations which were built against our datafeed functionality. For details on how to accept and process the XML payload, you can review the existing documentation here.

The only new functionality that the Webhooks version introduces beyond the documentation linked above is the ability to specify multiple endpoints from the “Integrations” section of the FoxyCart administration, and the automatic retries after failures. The API key used to decrypt the datafeed is still the key detailed on the “Advanced” settings page in the administration.

Zapier

Zapier.com is an automation service that allows you to create complex workflows that are executed when specific events occur. Zapier connects with hundreds of different services, and features a visual workflow creation tool allowing you to easily automate tasks and free up time.

Currently Foxy's integration with Zapier has one supported trigger, which is for each new transaction.

To make use of our Zapier integration, you will need to create your zaps within the Zapier administration and select our app from the list of trigger apps.

Foxy's Zapier integration is currently in private beta. If you would like access to the beta and be one of the first to try it out, please get in touch with us.

Once you've selected our app - simply follow the steps within Zapier's visual workflow editor to select the trigger, test the connection and build out the rest of your workflow. When setting up your FoxyCart trigger, it will attempt to fetch the latest transaction for your store to use as sample data when building out the rest of your zap. If your store doesn't have any transactions, you will need to complete a test transaction before completing the setup for your zap.

Webflow

Our Webflow webhook connects your FoxyCart store to your Webflow sites to introduce support for inventory management. This integration may be expanded in the future to support additional functionality as the Webflow API is expanded.

Setting up your Webflow collections

Before connecting Foxy to your Webflow site - you first need to create the necessary product collection within the Webflow CMS. For details on how to create collections - you can follow Webflow's guide on that here.

We also have a tutorial and starter template available for building a responsive ecommerce website with Webflow and Foxy - which you can review here. The tutorial and template will get you set up with a starting collection and templates ready to go for this integration.

If you're creating your own collections, when creating the Webflow collection for your products - there are some options you'll need as a minimum to work with Foxy, and we would recommend setting all of these options to also be required fields for creating products.

  1. A plain text field for the product name
  2. A number field for the product price
  3. A plain text field for the product code
  4. A number field for the product inventory, set to allow negative numbers

The name and price fields are the minimum values required to add a product to the cart, and the code and inventory fields are utilised for updating the inventory. If you don't have unique codes you can assign for each of your products, you could leave off the “code” field in your product collection, and instead utilise the “slug” attribute that Webflow automatically assigns to each of your products as the unique identifier in a later step.

Important to note, this integration assumes that all of your products are set up within a single Webflow collection, and that all of your products will be tracked for inventory. If the integration is unable to find a specific product code from an order in your products collection, it will trigger an error response for the webhook.

Customising your add to carts

When creating your add to cart links or forms within Webflow, you need to ensure that the Foxy integration knows which product it should update inventory for when it's processing a transaction. To do that, you'll need to ensure that you include the “code” value as part of your add to cart. You can also add an additional value to your form using the name quantity_max, and assign the value for that to be the “inventory” level for your product. This way, a customer is not able to order more of an item than you currently have.

If you used the ecommerce template linked to above, the template already includes the “code” attribute for you in the link within the products template - but if you're doing something custom, you can follow these steps to add it in.

If your products are added to the cart through a link, the additional parameters for your add to cart URL would be like this: &code=[ WEBFLOW_CODE ]&quantity_max=[ WEBFLOW_INVENTORY ].

If you are using a form, the additional fields would instead look like this:

<input type="hidden" name="code" value="[ WEBFLOW_CODE ]">
<input type="hidden" name="quantity_max" value="[ WEBFLOW_INVENTORY ]">

You will need to change both of those placeholder strings to the dynamic Webflow fields within the editor. The value for the [ WEBFLOW_CODE ] placeholder will either be the “code” field specified on the product collection, or if you didn't create one, use the “slug” attribute of your product that Webflow creates. For [ WEBFLOW_INVENTORY ], you'll set that to the “inventory” field you created for your products collection.

Once you've got the products collection created and the add to cart links added to your site, we'd recommend adding some products to the collection if you haven't already just so you have something to test with.

Enabling the Webflow webhook

Once you've got your collection created, you can then connect Foxy to your Webflow site by following these steps:

  1. On the Integrations section of your store's FoxyCart administration, check the checkbox for “Webflow” within the Webhooks section, and click the “Integrate Webflow”.
  2. You will be redirected to Webflow where you will need to login (if not already) using your Webflow login, and select which project you want to connect to Foxy.
  3. After selecting the desired project, you will be redirected back to the Foxy integrations page.
  4. From the dropdown within the Webflow section, select the Webflow collection for your products that you created above and click “Update”
  5. Next, select the code and inventory fields that you created earlier, and click “Update”. If you didn't create a code field, select the “slug” option instead.

Congratulations, you have now connected your Foxy store to Webflow! For any transactions completed in the future, each product in the cart will have the inventory value decreased in the matching item in the Webflow products collection (based on the product code).

Webflow webhook errors

Beyond the webhook errors noted earlier on this page, there are also errors specific to the Webflow integration that you may experience.

Specifically, if you have a product in a transaction which is missing a code attribute, or has a code attribute set which doesn't match an item in your products collection, you'll see an error like this within your store's error log:

Webhooks processing problems:
Your Site Name (webflow): Product code "my-missing-code" not found [400]

If you experience this error, you'll first want to confirm where the product was added to the cart and ensure that the link/form has the correct code associated with the product. To be able to re-feed the webhook to Webflow, you will need to update the product in the CMS to have the matching code to how it was sent.

If you've edited your product collections schema without publishing the page, Webflow will prevent any publishing requests via the API until the changes are published. If you experience this error, you'll see an error like this within your store's error log:

Webhooks processing problems:
Your Site Name (webflow): Conflict: This item can't be published, as your Collection structure changed since the last site publish. Make sure your staging and live Collections match to continue.

To fix this, simply ensure any changes to your collection's schemas have been published within Webflow, and then refeed the webhook to your Webflow site.

Site Tools