Integrate a Webform (3.x) with PayPal

For quite some time, I've wanted to integrate a particular webform with PayPal, since many nonprofits I help use that payment service. With older versions of PayPal, one could add some PHP code into a webform on the site to do this, but it was (a) hackish, and (b) a much less maintainable and secure way of accomplishing the goal I was trying to achieve. So I never did it.

However, after reading Additional processing in Drupal's Webform 3 module (from Drupal Coder), I found that Webform 3 has a hook that runs just after webform has saved a form's data to the database, but before the webform returns the user to a predefined redirected page. hook_webform_submission_insert() is the perfect time to hook into Webform's process and send a user to PayPal, along with that user's data.

PayPal has relatively robust documentation of it's Website Payments Standard API, and all we need to do is send an HTTP/HTTPS request to PayPal with certain parameters, defining to whom the payment should be sent, what the payment amount is, and some other options (including values sent in via our Webform).

In my site's custom module (I'm running Drupal 7 - code might need minor adaptations for 6.x), I added two simple hooks, shown below with comments to help define what's going on:

<?php
/**
 * Implements hook_webform_submission_insert().
 */
function custom_webform_submission_insert($node, $submission) {
  if (
$node->nid == 12) {

   
// Get mapping of form submission data using private function (see below).
    // Using this, we can use webform field names (like 'first_name') instead
    // of vague mappings like $submission->data['3']['value'][0].
   
$mapping = _custom_webform_component_mapping($node);

   
/**
     * Set up form to post to PayPal
     *
     * Preliminary notes:
     *  - PayPal expects a state code in 2-digit caps (no full names)
     *  - Need to break phone number into three strings... [area] [3-digits] [4-digits]
     *  - https://www.paypal.com/cgi-bin/webscr?cmd=_pdn_xclick_prepopulate_outside
     */
   
$paypal = array();
   
$paypal['cmd'] = '_donations'; // Varies depending on type of payment sent via PayPal
   
$paypal['business'] = '[email protected]'// PayPal account email
   
$paypal['bn'] = 'GM_Donate_WPS_US'; // See PayPal docs on proper formatting - not required, though
   
$paypal['page_style'] = 'primary'; // Set this in PayPal prefs, then change here (default = paypal)
   
$paypal['amount'] = $submission->data[$mapping['amount']]['value'][0];
   
$paypal['item_name'] = 'Description of the type of donation, payment, etc. goes here.';
   
$paypal['no_shipping'] = '1'; // Don't prompt user for shipping address
   
$paypal['no_note'] = '1'; // Don't prompt user for extra information (note)
   
$paypal['tax'] = '0'; // No tax for this payment
   
$paypal['rm'] = '1'; // Return method - 1 = browser redirected to return URL by GET method w/o variables
   
$paypal['return'] = 'http://example.com/thank-you'; // Page to which user is returned
   
$paypal['first_name'] = $submission->data[$mapping['first_name']]['value'][0];
   
$paypal['last_name'] = $submission->data[$mapping['last_name']]['value'][0];
   
$paypal['email'] = $submission->data[$mapping['email']]['value'][0];
   
$paypal['address1'] = $submission->data[$mapping['address']]['value'][0];
   
$paypal['address2'] = $submission->data[$mapping['address_2']]['value'][0];
   
$paypal['city'] = $submission->data[$mapping['city']]['value'][0];
   
$paypal['state'] = $submission->data[$mapping['state']]['value'][0];
   
$paypal['zip'] = $submission->data[$mapping['zip']]['value'][0];
   
$paypal['country'] = 'US';

   
// Build the URL/query for PayPal payment form.
   
$query = http_build_query($paypal, '', '&');
   
$url = 'https://www.paypal.com/cgi-bin/webscr?' . $query;

   
// Redirect user to PayPal...
   
drupal_goto($url);
  }
}

/**
 * Function to convert webform submissions into a nicely-mapped array.
 *
 * @see http://www.drupalcoder.com/story/678-additional-processing-in-drupals-webform-3-module
 */
function _custom_webform_component_mapping($node) {
 
$mapping = array();
 
$components = $node->webform['components'];
  foreach (
$components as $i => $component) {
   
$key = $component['form_key'];
   
$mapping[$key] = $i;
  }
  return
$mapping;
}
?>

The only thing I was not able to get working correctly was the phone number. Either you need to explicitly request each of the three parts of a user's phone number (area code, exchange, and number), or you need to parse the user-entered value into those three parts. One way makes it harder on the user—the other way on you!

Hopefully this will help many people who don't want to simply have the 'Donate' button (like political campaigners, who need more information) embedded on their sites, and also don't want to deal with the major overhead of having a whole shopping cart system on their site for one donation/payment form.

(The million dollar question is: does anyone want to take the time to make this functionality work in an addon module? The Webform payments module hasn't been updated since about a year ago... There's also Webform Pay, which has some traction, but doesn't support as many payment methods as I'd like—yet.).

Comments

Thanks! I remember noticing that back when it was released, but I hadn't remembered to look for it. That does hold some promise, but seems like it might not be the best solution for PayPal users. I've added a link in the main post, though.

Yes, I think there's an unsolved problem of how to get Pay module to work with payment processors that require a redirect to enter CC details. I've got a rough but working example module, will have to put it up on github if there isn't support in Pay yet.

That's great to know thanks. I've done exactly this hackish paypal processing in webforms many times and it's good to know there's a neater way to do it.

Hey Jeff,
cool post and thanks for sharing! Haven't dealt too much with paypal but never would have considered using the webform module to integrate with it.

keep up the good work,
pete

In which file is this code added? Will it allow me, for example, if I click on the Submit button in the webform to connect directly to paypal service, or need any payment gateway enabled on Drupal 6 to do so?

This would be added to your own custom module (in my case, custom.module). Doing it this way, you wouldn't need to use any extra modules or services to integrate with PayPal.

Jeff thx for sharing. Now building reservation form with paypal payment. And you code is very helpful. But i've never created modules. Can you guide me a bit?

I've created new folder : [webform_paypal]
created webform_paypal.info file

name = Webform Paypal
description = Integrate a Webform (3.x) with PayPal
dependencies[] = webform
core = 6.x

project = "webform_paypal"
package = Other

created webform_paypal.module with your code.

than enabled module in drupal. Created new webform. But after i press submit button it stays on conformation page. What am i doing wrong?

Replace all the instances of custom_ with webform_paypal_ in the code in this post, and you will probably have more luck :)

I thought that this might be the problem and renamed the folder/module/info file etc to [custom] as you have. Still no luck.

So this module supposed to work with all created webforms? maybe webform need special name? Just trying to understand what am i missing...

I have a conditional statement at the beginning, if ($node->nid == 12) {, and you'll need to set that to whatever nid you have for your form, or remove that statement if you want all forms to go to PayPal.

There's a lot of other little details that may be different for your site. You'll need to step through all the PHP to make sure you're getting the right values, too...

Thx Jeff for quick answers and usefull code! Very helpfull.

ps. just needed to change [$node->nid == 12] to my node ID.))) So easy))

Jeff if it's not to complicated for you please help me with this:

How can i transfer the "payment status"?
So currently user make the payment, it shown at seller paypal account(i use WPS) that the payments is received. But i'd like it to be shown on my website.

I was thinking about the way they do it in ubercart event registration module.
They have the field "payment status" that is changed to "completed" if the payment is made. All the posts i found on drupal about it are connected to ubercart and ecommerce.
But i like your module much more for my reservation/payment system.

thx for your time.

That is a topic for a future post - basically, you'll need to add on the ability for your module to track IPN notices, and mark a special/hidden field on the webform... I'm still trying to think of the best way to do this myself, as a 'registration lite' kind of module, as I typically don't want to go through the hassle of installing Ubercart or something similar just to allow people to submit one form...

Hi Jeff. I decided to use lm_paypal module for paypal IPNs.
If the IPN's payment status is [Complited] i want to change the hidden filed of the webform to PAID.

But couldn't find the way to do it... asked on drupal forum no help either...
somebody offered hook_form_alter(&$form, &$form_state, $form_id). But it Performs alterations [BEFORE] a form is rendered. In my case i need to change the value after the webform is submiited only when and if i get the IPN' payment status complited.

thx for your time Jeff, have a good one. Coolof

Jeff,
Wanted to take a moment to say thanks for posting the above information. I support a couple of church websites and needed a way to facilitate donations but needed the ability to capture some additional details and the webform module seemed like the most appropriate choice. However, one of the churches has an existing relationship with a Merchant Account Reseller (through Authorize.net) and didn't want to use paypal. I was able to modify your code so it could utilize Authorize.net's SIM payment model (similar to how Paypal works) without too much trouble (just more white hair!). Anyway, this posting got me going in the right direction and didn't require me to install additional interfaces to get webforms to work with Authorize.net. Thanks again.

Chris,
I know this is an old post, but I'll ask anyway. I'm trying to set this same thing up for a non-profit. May I ask how you did it?

Thanks for sharing this code.

I do see that the hook_webform_submission_insert() get's fired before the notification emails get sent. Any way around this?

Jeff, you are amazing for offering up this solution. Thank you.

This a great bit of code to knock out a quick solution to my problems. Have you done anything similar for shopping carts ? I can see wanting a simple programmatic cart filler without having a whole uber.

Unfortunately, no. But I feel your pain. Projects like Ubercart (and, in some ways, Drupal Commerce) are major overkill for extremely simple/easy shopping cart systems. I have built a few custom recurring payment modules, though.

Hi Jeff:
I was able to create a shopping cart programmatically after much searching about paypal and stackoverflow. I'm still new to hooking, so I used the webform / form settings / confirmation message, input format: php to code a purchase summary form which when clicked submits the items to a paypal cart for payment. For the ease I wanted the extra click at confirm is not a problem. Submit->Confirm->Paypal

The most beneficial information came from
http://stackoverflow.com/questions/1761803/submitting-multiple-items-to…

Instead of using all hidden fields, the confirmation form generated used some readonly text fields so the info was visible: This code previews real ugly but I tried

<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_cart">
<input type="hidden" name="upload" value="1">
<p>Please complete your online registration.<br/>Click the Pay Now button to send payment to:<br/>
<input type="text" readonly="true" name="business" value="[email protected]">
</p>
for each item_name_i and amount_i
print '<input type="text" readonly="true" name="item_name_'.$i.'" value="'.$item_name[i].'">'."\n";
      print '<input type="hidden" name="amount_'.$i.'" value="'.$amount[i].'"><br/>'."\n";
<p>
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_paynowCC_LG.gif" border="0" name="submit" alt="PayNow with PayPal - The safer, easier way to pay online!">
</p>
</form>

This is so awesome, and works fine in that it sends user to paypal after submitting form. I am wanting the form data to be sent as email and this drupal_goto($url) which send to paypal seems to cancel out the send of email. Any ideas?

Dustin: My experience is that I can use the Webform/Emails and add my address. Then I will get an email listing all the data entered when the form is submitted. I will get the email whether or not paypal was visited and/or was successful. I manually reconcile the webform submission against paypal notifications I receive. If I can't match a paypal to submission, I have to contact the person based on info they submitted ( so be sure to get an email )

Thank you for taking time to respond :) awesome!

So weird, yes it should work as you described, but emails are not going through, if I comment out the drupal_goto($url) then the email goes through, and then I uncomment and the emails do not go through, is there some setting somewhere that be causing this?

Might you have some other process or hook interfering with webform? Do you have any webform addon modules installed? The email should still be sent before the user is sent off to PayPal.

So I just tried a clean install of drupal 7.8 with webform 7.x-3.13 on different hosted server and have this same issue happening, what versions are you using?

I am using 6.22. Remember I am not using a hook and placing my php code that generates [the 'order review' and link to paypal cart] in the Webform/Form Settings/Confirmation Message. Again, not the best place to put such code but it worked on the time crunch I had.

Your problem I speculate is that that drupal_goto($url) is forcing the browser to leave your site before the site has a chance to reach the email sending portion of webforms.

In webform.module function webform_client_form_submit($form, &$form_state) has a section

// Save the submission to the database.

if (!$sid) {
// No sid was found thus insert it in the dataabase.
$form_state['values']['details']['sid'] = $sid = webform_submission_insert($node, $submission);

below which is

// Check if this form is sending an email.
if (!$is_draft && !$form_state['values']['details']['finished']) {
$submission = webform_get_submission($node->nid, $sid, TRUE);
webform_submission_send_mail($node, $submission);
}

webform_submission_insert() is in includes/webform.submissions.inc and that is from where the insert hook is invoked -- which jumps the page away before the email section can run.

Try replicating the email code inside your hook just prior to when you call drupal_goto($url). Hope that works!

Okay cool, well I thought your tutorial was all about using the hooks, but anyway your info was enough for me to dig into the webform module and add my own hook after the email is sent off which then implements your code above and sends the user to paypal before the redirect. climbing one more rung up the drupal noob ladder :)

So awesome thanks for the insight!!

No problem! You might want to ask in the webform issue queue if your new hook could be incorporated into the module. If it benefitted you, it may benefit other users as well. And you can get help in getting everything working without patching a module in the future...

Hi Dustin,

I'm facing similar problem. How did you finally implement the hooks? Did you create another hook (maybe: webform_paypal_webform_submission_send_mail($node, $submission)) in the same module then call it from the first hook (webform_paypal_webform_submission_insert)?

Hey Albert, the final solution was to put this line in the custom module before the drupal_goto, so it sends an email first then does the redirect

webform_submission_send_mail($node, $submission);

// Redirect user to PayPal...
drupal_goto($url);

Thanks for the reply Dustin.
I also just tested the reply from Curtis below which is the same with what you did (he used it for Drupal 7), and found that it also works for Drupal 6.
Thanks again.

For anyone still having email send issues with this code using Drupal 7 I was able to get it working by adding this line:

webform_submission_send_mail($node, $submission);

above

drupal_goto($url);

Thanks Jeff!

Cheers,

Curtis

Thanks goes to Jeff for this very powerful addition to Webform & to others for these comments-- troubleshooting setup & issues. This is exactly how the Drupal issue queues address fixes & additions, so I highly recommend someone makes this available to others on Drupal.org so it can be improved by the open-source community.

I use this for two applications on one non-profit website for a client. Thousands are collected through the module in donations and membership dues. I have been using simple IF statements on radio buttons to select payment types with this script.

The email fix noted by Curtis here does fix a problem with the current version of Webform / Drupal. It had worked without the fix before, but upgrading something had caused the emails to not go through. Reasons like this are why I recommend this script is handed over to the Drupal community so it can be maintained.

If that isn't Jeff, whoever does the commit should credit the initiation of this to Jeff.

I would love to try to get something put together as far as a module goes, but I don't really have a need for it at this time, and this method is a little too custom to be quickly turned into a module. However, projects like Webform Pay and a few others could use some help, and might be a better/more flexible solution in the end.

Thanks so much! I've been looking for something like this for a long time. I'm not a developer and I've never written a module, but between your code and the .info content provided in Coolof's comment, I had no problem with this.

Fantastic work Jeff, this has been a huge help. I have a question about submitting data to an external URL separate from the redirection. Basically I need to submit data to a specific URL in a third party service, like this:

http://sample.com/service/add?token=1234&firstname=Woodrow&lastname=Wil…

and then redirect the user to a local confirmation page. I've been suggested to use curl, and I've got that working by removing drupal_goto() and using this code:

// Directs the URL to the external service
$timeout = 60;
$service_output = curl_init();
curl_setopt($service_output, CURLOPT_URL, $url);
curl_setopt($service_output, CURLOPT_RETURNTRANSFER, true);
curl_setopt($service_output, CURLOPT_CONNECTTIMEOUT, $timeout);
$service_return = curl_exec($service_output);
curl_close($service_output);

if (empty($service_return)) {
// Service did not respond, print error
} else {
// Service responded, print success
}

Although I have yet to flesh out or implement the $service_return conditionals (or even know if that would work?), it is hitting the external url and going to the webform confirmation page.

Does this seem like the best approach? Or would you suggest another method?

I would probably take that approach. cURL is typically a bit intimidating at first, but if you can wrap your head around some of the basics, I think you'll find it's great for doing things like what you're doing...

Jeff, I hope you can help me out. I'm using Drupal 7.10 and Webform 7.x-3.17. I've been using your solution for several month and it's worked beautifully (thank you!). However, it has suddenly stopped working. On submit, it goes to a Paypal error page saying that the transaction could not be completed. Have you heard of this happening before? Any idea what to do? Thanks.

Sounds like maybe PayPal has changed something in their API (not too likely), or the Webform module's data is a little different. You should use the Devel module to allow easy inspection of the arrays that you're pulling the form information from. Just use the dpm() function to inspect things (works similar to print_r(), but much easier on the eyes).

Thanks. I looked at the Paypal API, and as far as I can tell nothing has changed. I haven't been using the Devel module, but it sounds like I need to learn how.

It's a major time-saver. Just enable the module, then put a line like devel($some_array_of_info); in your module code, and refresh the page (or re-submit the webform). Then on the page load, you'll see a nice clickable array of information, so you can see whether you need to get a variable like:

<?php
  $my_array
['info'][0]['item'];
?>

or

<?php
  $my_array
['info'][0]['item']['another_level_of_array'];
?>

Ok, I'm new to drupal and I'm currently unsure how do I implement this option into the webform I created. Which file in my modules folder do I change or am I thinking about it all wrong?

Sorry, but I'm unable to help at this time due to other obligations. You may be able to find another Drupal developer who could help you complete your project—you can browse the Drupal.org marketplace or check on IRC.

I actually got it all setup now, the only problem is that every time I try to enable the module. It gives me this error " Parse error: syntax error, unexpected T_IF in /Users/Debo/Sites/fffa/sites/all/modules/custom/custom.module on line 6".

Seems like it's just this step left.

Ok I really tried to get this to work building the module but I'm getting an error from line 6 of the .module file I created where we declare the node id. What am I doing wrong?

I GOT IT! Ecstatic! There was a bunch of errors in the code. It seems like there were hidden characters in the code. I loaded the code in dreamweaver and it highlighted all the lines in red that had problems. I pretty much retyped them all and it worked. Thanks for writing this code and leaving me to figure it out, it pushed me to learn it for myself.

Once I started going through the documentation, it wasn't as daunting as I thought it would be to relate it back and then figure out the problems.

This worked great for me for a while. But now it won't head to PayPal upon clicking Submit. Is it because of the new Webform 4, or did I make some mistake? Webform 4 does have some changes from Webform 3, but I don't know if they are significant. Ideas?

Never mind. Works fine. For some reason I had the module disabled. (I did make some adjustments earlier, changing ['Value'][0] to [0], per the new Webform 4 standard. Not sure if that mattered or not.

Thanks for this!!
I got the phone number to work using $paypal['H_PhoneNumber'].

Hi Jeff, after lot of google search i found your code and it works like awesome. only thing is after i click the submit button, the values entered are stored in my db, whether i pay not pay the amount in paypal. how to restrict webform from saving data before the paypal transaction!

You wouldn't be able to do this very easily, because once the user is redirected to PayPal, Drupal would lose that user's data. You'd need to hook into Webform, save the data elsewhere, then wait for PayPal to tell your site the payment is complete before moving the data back into Webform's storage.

Another alternative would be to add a separate field somewhere to track payments for submissions, and update that field when PayPal reports the payment is complete.

Hello. Thank you for module. I use webform as block for one content type (used on many pages). how to tune your module in this case?
Thanks in advance.

This was exactly what I was looking for - easily adapted for Worldpay use, and works fine with Drupal 6 - no need to change hooks.

Thanks a lot!