less than a minute read • Updated an hour ago
Verify and decrypt a legacy webhook payload
How to validate a legacy webhook payload signature and decrypt it if your endpoint isn't secured with HTTPS.
Once configured, a JSON payload is sent to your endpoint whenever a subscribed event occurs. If your endpoint is secured with HTTPS, the payload is sent unencrypted, ready to use. If your endpoint isn’t secured, the payload is encrypted with AES-256-CBC before being sent, and needs to be decrypted before use.
Headers
Every legacy webhook request includes these headers:
Header | Description |
|---|---|
| Name of the event that triggered this payload. Currently |
| An HMAC SHA256 signature of the payload, using the webhook’s encryption key. Used to verify the contents of the payload |
| A boolean signifying whether this payload has been refed. If false, this is the first time this instance of the event has been triggered |
| The ID of the store this webhook was triggered for |
| The store domain this webhook was triggered for |
Example payloads
The payload follows the same structure as the Hypermedia API. Most objects include the API’s _links array, which contains URIs you can use to access the resource in the API if you’re also using it — these can be safely ignored otherwise. In the examples below, _links arrays have been cleared out to save space.
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
}
],
"fx:payments": [
{
"_links": { },
"amount": 54.2,
"cc_exp_month": "01",
"cc_exp_year": "2023",
"cc_number_masked": "xxxxxxxxx4242",
"cc_type": "Visa",
"date_created": null,
"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_code": "USD",
"currency_symbol": "$",
"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",
"user_agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
}
Two extra attributes help identify the transaction:
transaction_type— what kind of transaction this was:transaction,updateinfo,subscription_modification,subscription_renewal, orsubscription_cancellationis_new_subscription— a boolean indicating whether a new subscription is included in this transaction
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
}
Decrypting an encrypted payload
If your webhook endpoint URL isn’t secured with SSL, the payload is sent encrypted rather than raw. The payload arrives as three hexadecimal values — the MAC (Message Authentication Code), IV (Initialization Vector), and the encrypted data — concatenated with a : character.
To decrypt it:
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
Notes
Avoid storing your webhook encryption key directly in your codebase — store it as an environment variable instead.
Always use a secure/constant-time comparison method when comparing signatures or MAC values, rather than a direct equality check.