====== Pre-Payment Webhook ====== 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: * Final inventory checks for low-stock or one-of-a-kind products. * Custom validations to ensure that certain product configurations are present. * Integrations with custom validation or fraud-check services (such as [[https://wiki.foxycart.com/integration/fraudlabspro|FraudLabsPro]]). ===== Notes ===== Currently, the pre-payment webhook is not triggered for subscription renewals. If you have a use case where this is needed, please [[https://foxy.io/contact|contact us]]. ===== Enabling the pre-payment Hook ===== To enable the pre-payment hook, head to the "payment" setting page in your store's FoxyCart administration, ensuring that you've selected the [[.:payment_sets|payment set]] you're wanting to enable the hook for. Within the "Anti-Fraud Integrations" section, check the checkbox to "enable custom pre-payment hook" and complete the settings below. ; pre-payment hook url : The URL for the endpoint script, which needs to be HTTPS, which will receive the JSON payload. ; failure handling : If your endpoint script fails to load or respond correctly, use this setting to set whether the transaction should be automatically approved or rejected. If set to reject, a generic error message will be returned, which you can edit the error from the "language" section of your store's administration - look for the "error precheckout hook" language string within the "checkout" group. ===== Handling the request ===== ==== 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 ''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. | ==== Events ==== 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: * [[gateways:opayo#special_considerationprepayment_webhook|Opayo]] * [[gateways:barclaycard#special_considerationprepayment_webhook|Barclaycard]] ==== Example Payload ==== 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 [[https://api.foxycart.com|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" } ==== Notes ==== * The payload includes several ''_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. ===== Sending a response ===== In response, FoxyCart expects a JSON payload in the following format to be output on the page (prettified for display purposes): ==== Approve ==== { "ok": true, "details": "" } Note that if you're using [[/gateways:square_platform|the Square Platform payment splitting integration]], an additional parameter of ''payment_logic'' can be passed through as well. ==== Reject ==== { "ok": false, "details": "Sorry, we're all out of Example Products. Please remove that item from your cart and try again." } {{ :v:2.0:transaction_hook_error.png?nolink&600 |}} 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. ===== Example Endpoint ===== 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: 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); ===== Debugging Errors ===== 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.