The pre-payment hook functionality sends a request to an endpoint of your choosing right before Foxy would otherwise submit the payment to the payment gateway, and allows for stores to apply custom validations for transactions prior to the request being set to the chosen payment method. A payload containing information about the current transaction is sent to the endpoint configured for this functionality, allowing the endpoint to respond to either approve or reject the transaction. If approved, the transaction will be sent on to the gateway to process, or if rejected the customer will be sent back to the checkout with a custom error message.
Common use cases for the pre-payment hook include:
Currently, the pre-payment webhook is not triggered for subscription renewals. If you have a use case where this is needed, please contact us.
To enable the pre-payment hook…
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 validation/payment or validation/3ds. |
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. |
The prepayment webhook is primarily triggered right before the payment is sent to your gateway for handling. For this trigger, the Foxy-Webhook-Event header will be validation/payment.
For a couple gateways though, we do also trigger the prepayment webhook prior to the 3DSv2 challenge occurring, which has an event header of validation/3ds. These triggers exist for specific purposes, and are documented on the respective gateway wiki pages:
When the customer attempts to complete their purchase, after the Google reCAPTCHA is validated (if active), a POST request is sent off to your custom post-checkout hook endpoint with a JSON payload representing the current cart. It follows the same structure as our Hypermedia API, as the majority of the data comes from there.
The following is an example of the JSON payload, showing two products (with one having live shipping rates) and having a coupon added.
{ "_links": { }, "_embedded": { "fx:items": [ { "_links": { }, "_embedded": { "fx:item_options": [ { "_links": { }, "name": "size", "value": "medium", "price_mod": 0, "weight_mod": 0, "date_created": null, "date_modified": null }, { "_links": { }, "name": "color", "value": "red", "price_mod": 0, "weight_mod": 0, "date_created": null, "date_modified": null } ], "fx:item_category": { "_links": { }, "admin_email_template_uri": "", "customer_email_template_uri": "", "code": "DEFAULT", "name": "Default for all products", "item_delivery_type": "flat_rate", "max_downloads_per_customer": 3, "max_downloads_time_period": 24, "default_weight": 0, "default_weight_unit": "LBS", "default_length_unit": "IN", "shipping_flat_rate_type": "per_item", "shipping_flat_rate": 5, "handling_fee_type": "none", "handling_fee": 0, "handling_fee_minimum": 0, "handling_fee_percentage": 0, "customs_value": 0, "discount_type": "", "discount_name": "", "discount_details": "", "send_customer_email": false, "send_admin_email": false, "admin_email": "", "date_created": null, "date_modified": null } }, "item_category_uri": "https://api.foxycart.com/item_categories/100", "name": "Example Product", "price": 15.99, "quantity": 1, "quantity_min": 0, "quantity_max": 0, "weight": 0, "code": "abc123", "parent_code": "", "discount_name": "", "discount_type": "", "discount_details": "", "subscription_frequency": "", "subscription_start_date": null, "subscription_next_transaction_date": null, "subscription_end_date": null, "is_future_line_item": false, "shipto": "Me", "url": "", "image": "", "length": 0, "width": 0, "height": 0, "expires": 0, "date_created": null, "date_modified": "2017-07-20T04:13:08-0700" }, { "_links": { }, "_embedded": { "fx:item_category": { "_links": { }, "admin_email_template_uri": "", "customer_email_template_uri": "", "code": "live", "name": "Live Rates", "item_delivery_type": "shipped", "max_downloads_per_customer": 3, "max_downloads_time_period": 24, "default_weight": 5, "default_weight_unit": "LBS", "default_length_unit": "IN", "shipping_flat_rate_type": "per_order", "shipping_flat_rate": 1, "handling_fee_type": "none", "handling_fee": 0, "handling_fee_minimum": 0, "handling_fee_percentage": 0, "customs_value": 0, "discount_type": "", "discount_name": "", "discount_details": "", "send_customer_email": false, "send_admin_email": false, "admin_email": "", "date_created": null, "date_modified": null } }, "item_category_uri": "https://api.foxycart.com/item_categories/101", "name": "Another Product", "price": 20, "quantity": 1, "quantity_min": 0, "quantity_max": 0, "weight": 5, "code": "foo321", "parent_code": "", "discount_name": "", "discount_type": "", "discount_details": "", "subscription_frequency": "", "subscription_start_date": null, "subscription_next_transaction_date": null, "subscription_end_date": null, "is_future_line_item": false, "shipto": "Me", "url": "", "image": "", "length": 0, "width": 0, "height": 0, "expires": 0, "date_created": null, "date_modified": "2017-07-20T04:14:03-0700" } ], "fx:discounts": [ { "code": "coupon", "amount": -3.6, "name": "Default Discount", "display": "-$3.60", "is_taxable": false, "is_future_discount": false } ], "fx:custom_fields": [ { "name": "custom_note", "value": "Happy Birthday!", "is_hidden": 0 }], "fx:shipment": { "address_name": "", "first_name": "John", "last_name": "Smith", "company": "", "address1": "Main Street", "address2": "", "city": "Saint Paul", "region": "MN", "postal_code": "55116", "country": "US", "origin_region": "TX", "origin_postal_code": "77018", "origin_country": "US", "shipping_service_id": 0, "shipping_service_description": "", "is_residential": false, "item_count": 2, "total_weight": 5, "total_customs_value": 0, "total_handling_fee": 0, "total_flat_rate_shipping": 5, "total_item_price": 35.99, "total_tax": 3.24, "total_shipping": 0, "total_price": 39.23 }, "fx:customer": { "id": "1512345", "first_name": "John", "last_name": "Smith", "email": "john@example.com", "tax_id": "", "is_anonymous": "0", "_embedded": { "fx:payments": [ { "cc_type": "plastic", "cc_number_masked": "xxxx xxxx xxxx 4242", "cc_exp_month": "10", "cc_exp_year": "2020", "purchase_order": null } ], "fx:default_billing_address": { "country": "US", "region": "MN", "city": "Saint Paul", "postal_code": "55116", "address1": "Main Street", "address2": "", "company": "", "full_name": "John Smith", "first_name": "John", "last_name": "Smith", "phone": "" } } } }, "customer_uri": "", "template_set_uri": "", "language": "", "locale_code": "en_US", "customer_ip": "192.168.0.1", "ip_country": "United States", "session_name": "fcsid", "session_id": "hvcv28l8md0qc8qt5rrjh4qo85", "total_item_price": 35.99, "total_tax": 3.24, "total_shipping": 14.23, "total_future_shipping": 0, "total_order": 49.86, "date_created": null, "date_modified": "2017-07-20T04:12:25-0700" }
_links arrays. These contain helpful URI's that could be used through the Hypermedia API if you're also making use of that. If not, these can be safely ignored.FoxyCart expects a response from your endpoint within 20 seconds of the request being sent - so you will need to ensure that any logic that your endpoint undertakes is completed quickly. Remember that as this request happens after the customer clicks to complete their transaction, any extended delays with your endpoint will mean they will also be waiting longer for a response to their transaction.
In response, FoxyCart expects a JSON payload in the following format to be output on the page (prettified for display purposes):
{ "ok": true, "details": "" }
Note that if you're using the Square Platform payment splitting integration, an additional parameter of payment_logic can be passed through as well.
{ "ok": false, "details": "Sorry, we're all out of Example Products. Please remove that item from your cart and try again." }
Only a valid JSON object should be output from your custom endpoint. If you output other elements to the page that is not part of a valid JSON object, the hook will fail to process and the default handling will occur.
The following is an example PHP endpoint that could be used to handle the pre-payment hook. Note that this is for illustrative purposes only. It'll work as is, but blocking based on IP or email isn't necessarily a recommended approach:
<?php $rawPost = file_get_contents('php://input'); $cart_details = json_decode($rawPost, true); $response = array( 'ok' => true, 'details' => "Sorry, we couldn't process your transaction. Please contact us to proceed." ); $log_file = './log.txt'; $date = new DateTime(); $date_string = $date->format('Y-m-d H:i:s'); $log_line = $date_string . ': ' . $cart_details['customer_ip'] . ' - '. $cart_details['_embedded']['fx:customer']['email'] . ' -- '; // Example: Loop through cart items and reject on specific product names foreach($cart_details['_embedded']['fx:items'] as $item) { if ($item['name'] == 'Example Product') { $response['ok'] = false; $response['details'] = "Sorry, we're all out of Example Products. Please remove that item from your cart and try again."; } } // Example: Reject on specific IP addresses // The IPs to block should be in a separate file named `ips_to_reject.txt` // Note that this looks for exact matches. // If you wanted to check CIDR ranges, try adding something like https://github.com/tholu/php-cidr-match $ips_to_reject = file('./ips_to_reject.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($ips_to_reject) { foreach ($ips_to_reject as $ip) { if ($ip == $cart_details['customer_ip']) { $log_line .= "IP MATCH: $ip"; $response['ok'] = false; break; } } } // Example: Reject on specific email addresses (exact matches) // The emails to block should be in a separate file named `emails_to_reject.txt` $emails_to_reject = file('./emails_to_reject.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($emails_to_reject) { foreach ($emails_to_reject as $email) { if (trim($email) == trim($cart_details['_embedded']['fx:customer']['email'])) { $log_line .= "EMAIL MATCH. REJECTING."; $response['ok'] = false; break; } } } $log_line .= (string) $response['ok'] . "\n"; $fp = fopen($log_file, 'a'); fwrite($fp, $log_line); header('Content-Type: application/json'); print json_encode($response);
If your pre-payment hook endpoint fails to return a response, or returns a non-JSON response, a error will be added to your store's error log in the administration.