diff --git a/commerce_paytrail.module b/commerce_paytrail.module
index 33f594f..378564b 100644
--- a/commerce_paytrail.module
+++ b/commerce_paytrail.module
@@ -18,7 +18,7 @@ define('PAYTRAIL_DEFAULT_CULTURE', 'fi_FI');
 define('PAYTRAIL_DEFAULT_TYPE', 'S1');
 define('PAYTRAIL_DEFAULT_RETURN_SUCCESS', 'checkout/!order_id/payment/return/!payment_redirect_key/success');
 define('PAYTRAIL_DEFAULT_RETURN_CANCEL', 'checkout/!order_id/payment/return/!payment_redirect_key/cancel');
-define('PAYTRAIL_DEFAULT_RETURN_NOTIFY', 'checkout/!order_id/payment/return/!payment_redirect_key/notify');
+define('PAYTRAIL_DEFAULT_RETURN_NOTIFY', 'commerce_paytrail_notify/!order_id/!payment_redirect_key');
 define('PAYTRAIL_DEFAULT_EMBED', 'embed');
 
 /**
@@ -41,6 +41,19 @@ function commerce_paytrail_commerce_payment_method_info() {
 }
 
 /**
+ * Implements hook_menu().
+ */
+function commerce_paytrail_menu() {
+  $items['commerce_paytrail_notify/%/%'] = array(
+    'page callback' => 'commerce_paytrail_notify_check',
+    'page arguments' => array(1, 2),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
  * Payment method callback; return the settings form for a payment method.
  *
  * @param $settings
@@ -113,7 +126,7 @@ function commerce_paytrail_settings_form($settings = NULL) {
   $form['advanced']['return_notify'] = array(
     '#type' => 'textfield',
     '#title' => t('Return address (notify)'),
-    '#description' => t('<strong>Available variables:</strong> !order_id, !payment_redirect_key<br /><strong>Default value:</strong> <em>checkout/!order_id/payment/return/!payment_redirect_key/notify</em>'),
+    '#description' => t('<strong>Available variables:</strong> !order_id, !payment_redirect_key<br /><strong>Default value:</strong> <em>commerce_paytrail_notify/!order_id/!payment_redirect_key</em>'),
     '#default_value' => !empty($settings['advanced']['return_notify']) ? $settings['advanced']['return_notify'] : PAYTRAIL_DEFAULT_RETURN_NOTIFY,
   );
 
@@ -198,6 +211,30 @@ function commerce_paytrail_redirect_form($form, &$form_state, $order, $payment_m
 }
 
 /**
+ * Generate checksum from return arguments.
+ *
+ * @param array $response
+ *   Array of url arguments.
+ *
+ * @return null|string
+ *   Hashed checksum.
+ */
+function commerce_paytrail_return_checksum(array $response) {
+  if (!isset($response['ORDER_NUMBER'], $response['TIMESTAMP'], $response['PAID'], $response['METHOD'])) {
+    return;
+  }
+  // Generate checksum with md5, convert to uppercase.
+  $checksum_string = format_string('@order|@timestamp|@paid|@method|@merchant_hash', array(
+    '@order' => $response['ORDER_NUMBER'],
+    '@timestamp' => $response['TIMESTAMP'],
+    '@paid' => $response['PAID'],
+    '@method' => $response['METHOD'],
+    '@merchant_hash' => _commerce_paytrail_get_setting('merchant_hash'),
+  ));
+  return drupal_strtoupper(md5($checksum_string));
+}
+
+/**
  * Payment method callback; validation callback for redirected payments.
  *
  * Gets called when user comes back from offsite payment.
@@ -213,26 +250,12 @@ function commerce_paytrail_redirect_form_validate($order, $payment_method) {
   }
 
   // Select validation mode based on transaction return status code.
-  if (arg(5) == 'success' || arg(5) == 'notify') {
+  if (arg(5) == 'success') {
     $response = $order->data['commerce_paytrail']['response'];
-
-    // Get response values.
-    $return_values = array(
-      'ORDER_NUMBER' => check_plain($response['ORDER_NUMBER']),
-      'TIMESTAMP' => check_plain($response['TIMESTAMP']),
-      'PAID' => check_plain($response['PAID']),
-      'METHOD' => check_plain($response['METHOD']),
-      'RETURN_AUTHCODE' => check_plain($response['RETURN_AUTHCODE']),
-    );
-
-    // Get merchant hash.
-    $merchant_hash = _commerce_paytrail_get_setting('merchant_hash');
-
-    // Generate checksum with md5, convert to uppercase.
-    $checksum = drupal_strtoupper(md5($return_values['ORDER_NUMBER'] . '|' . $return_values['TIMESTAMP'] . '|' . $return_values['PAID'] . '|' . $return_values['METHOD'] . '|' . $merchant_hash));
+    $checksum = commerce_paytrail_return_checksum($response);
 
     // Check checksum validity.
-    if ($checksum != $return_values['RETURN_AUTHCODE']) {
+    if ($checksum != $response['RETURN_AUTHCODE']) {
       drupal_set_message(t('Validation failed (security hash mismatch). Please contact store administration if the problem persists.'), 'error');
       $failure = TRUE;
     }
@@ -266,18 +289,11 @@ function commerce_paytrail_redirect_form_validate($order, $payment_method) {
 function commerce_paytrail_redirect_form_submit($order, $payment_method) {
 
   $return_type = arg(5);
-  if ($return_type == 'success' || $return_type == 'notify') {
+  if ($return_type == 'success') {
     // Payment successful at Paytrail, set transaction status to 'verified' to initiate order delivery.
     $transaction = commerce_payment_transaction_load($order->data['commerce_paytrail']['transaction']->transaction_id);
     $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
     commerce_payment_transaction_save($transaction);
-
-    /* The notify handler is called by Paytrail, not via the user's browser.
-      We only need to show a page with HTTP 200 headers to signal that the response has been processed. */
-    if ($return_type == 'notify') {
-      commerce_payment_redirect_pane_next_page($order); // Manually advance the order status, in this case to checkout_complete
-      exit();
-    }
   }
 }
 
@@ -472,3 +488,81 @@ function _commerce_paytrail_get_setting($setting_name) {
   return $setting;
 }
 
+/**
+ * Save the transaction for notify url for Paytrail.
+ *
+ * @param int $order_id
+ *   Order ID.
+ *
+ * @return bool
+ *   TRUE on success, FALSE on failure.
+ */
+function commerce_paytrail_notify_save($order_id) {
+  if (!$order = commerce_order_load($order_id)) {
+    return FALSE;
+  }
+  if (empty($order->data['commerce_paytrail']['transaction']->transaction_id)) {
+    return FALSE;
+  }
+  $transaction = commerce_payment_transaction_load($order->data['commerce_paytrail']['transaction']->transaction_id);
+  $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
+  commerce_payment_transaction_save($transaction);
+
+  // Manually advance the order status, in this case to checkout_complete.
+  commerce_payment_redirect_pane_next_page($order);
+
+  return TRUE;
+}
+
+/**
+ * Return 500 error code and log to watchdog.
+ *
+ * @param string $message
+ *   Message to log.
+ * @param array $arguments
+ *   Arguments for watchdog.
+ */
+function commerce_paytrail_notify_error($message, $arguments = array()) {
+  watchdog('commerce_paytrail', $message, $arguments);
+  drupal_add_http_header('Status', '500 Internal Server Error');
+  drupal_exit();
+}
+
+/**
+ * Check whether the received data is valid.
+ *
+ * @param string $order_id
+ *   The order id.
+ * @param string $payment_redirect_key
+ *   The payment redirect key.
+ */
+function commerce_paytrail_notify_check($order_id, $payment_redirect_key) {
+  $response = $_REQUEST;
+  // Validate url arguments.
+  if (!isset($response['ORDER_NUMBER'], $response['TIMESTAMP'], $response['PAID'], $response['METHOD'])) {
+    return commerce_paytrail_notify_error('Notify failed: missing required url parameter.');
+  }
+  // Make sure order exists.
+  if (!$order = commerce_order_load($order_id)) {
+    return commerce_paytrail_notify_error('Notify failed: order not found (@id).', array(
+      '@id' => $order_id,
+    ));
+  }
+  // Validate redirect key.
+  if (empty($order->data['payment_redirect_key']) || $order->data['payment_redirect_key'] !== $payment_redirect_key) {
+    return commerce_paytrail_notify_error('Notify failed: redirect key mismatch.');
+  }
+  $checksum = commerce_paytrail_return_checksum($response);
+
+  // Check checksum validity.
+  if (empty($response['RETURN_AUTHCODE']) || $checksum !== $response['RETURN_AUTHCODE']) {
+    return commerce_paytrail_notify_error('Notify failed: checksum mismatch (@checksum, @return_auth)', array(
+      '@checksum' => $checksum,
+      '@return_auth' => $response['RETURN_AUTHCODE'],
+    ));
+  }
+  if (!commerce_paytrail_notify_save($order->order_id, $response)) {
+    return commerce_paytrail_notify_error('Notify failed: failed to save transaction.');
+  }
+  drupal_exit();
+}
