Using PayPal in Meteor

Can someone walk me through setting up PayPal in a Meteor application?

I signed up for the developer account and looked over some blog posts, but everything seems to be older. From what I’ve read, for this, I don’t need the package (which hasn’t been updated in a while).

Any help is appreciated.

1 Like

Hi @aadams,

Don’t know if this helps but… I found this:

This was posted 6 months ago. I might play with it while waiting for Stripe to be available in my country.

Thanks, I’ll read that.

What exactly are you trying to do? Just add a payment button? Process payments on your end or just embed Paypal and let them do the heavy lifting and deal with CPI compliance and have a callback to your site?


I found this article:

And got the button working. I’m able to send the payment over to paypal. But after paying with the sandboxed account, I can get paypal site to redirect to a complete page in my Meteor application… but I cannot get my server side route to run and give me transaction details to add to my database.

// both/controller.js

paypalPaymentController = RouteController.extend({
  template: 'paypal-payment'

// both/routes.js

Router.route('/paypalCallback', function (req, res) {'callBackendCode', this.params, function(err, res) {
    console.log('got the response');
}, {name: 'paypal-callback', where: 'server'});

Router.route('/paypal-trans-complete', function (req, res) {
  // your return url direct your customer after the successful payment with some return information.
  console.log('successful payment, return info, ', req);

// server/methods.js

  callBackendCode: function (params) {
    console.log('you sent up the parameters' + params);
    // console.log(‘you sent up the parameters ’, params);
    //here you will do all your paypal tracking. The params should have information regarding the customer.

// paypal.html

<template name='paypal-payment'>
	<form action="" class="paypal-button" method="post" style="opacity:" target="_top">
    <div class="hide" id="errorBox"></div>
    <input name="button" type="hidden" value="buynow"> 
  	<input name="business" type="hidden" value=""> 
  	<input name="item_name" type="hidden" value="stuff">
    <input name="quantity" type="hidden" value="1"> 
    <input name="amount" type="hidden" value="2.00"> 
    <input name="currency_code" type="hidden" value="USD"> 
    <input name="shipping" type="hidden" value="0.00"> 
    <input name="tax" type="hidden" value="0.00">
    <input name="notify_url" type="hidden" value="http://localhost:3000/paypalCallback">
    <input type="hidden" name="return" value="http://localhost:3000/paypal-trans-complete">
    <input name="env" type="hidden" value="www.sandbox"> 
    <input name="cmd" type="hidden" value="_xclick"> 
    <input name="bn" type="hidden" value="JavaScriptButton_buynow"> 
    <button style='border-color:transparent;background-color:transparent;'>
    	<img type="submit" src="" alt="Buy now with PayPal" />

I thought the <input name="notify_url" type="hidden" value="http://localhost:3000/paypalCallback"> would send me back to my Meteor application (in my case a server side route).

But the only thing that runs after is <input type="hidden" name="return" value="http://localhost:3000/paypal-trans-complete">.

The Paypal API is ancient. If you want to accept Paypal payments without a big effort, use Braintree. Their API is stellar and easy to get up and running. They are also owned by Paypal.
Also you get new features that aren’t even available through the regular legacy Paypal API.

I think the issue is that PayPal will not send the IPN to your localhost from what I can tell.

From this post,

Since the IPN listener accepts form input from a PayPal message, you can emulate that yourself locally to test your listener script by using HTML code similar to the code shown below. You just create a test page that submits a form with real field names and test values to emulate the data that might be sent by PayPal in an IPN message.

<form target="_new" method="post" action="">
<input type="hidden" name="SomePayPalVar" value="SomeValue1"/>
<input type="hidden" name="SomeOtherPPVar" value="SomeValue2"/>

<!-- code for other variables to be tested ... -->

<input type="submit"/>

Once you have created your initial listener, you can verify that your the listener’s “verified” code works as expected. To do that, copy the code that runs if PayPal’s response is VERIFIED to the part of your listener that runs if a response is INVALID. Next, post a dummy IPN to PayPal using form code as described above. Because a dummy IPN causes PayPal to return an INVALID message (since the message did not originate from PayPal), your “invalid” code will run, not your “verified”. So temporarily moving your “verified” code to the “invalid” code section gives you a chance to check it locally, before moving to the next phase of the testing.

Sure, basically all you have to do is to use the braintree npm module + clientside js. We are using bt to process subscription payments but single payments should be even simpler.
Rough outline:

Create a server method that authenticates with their gateway, get the client token and pass that to the clientside js. Once you have the paymentToken, pass that into another server method that uses the gateway to perform the transaction.

That’s great! Braintree opened a public beta in Singapore, Malaysia and Hong Kong.

I’m trying to do a POST back to paypal and I’m having issues.

Router.route('/paypal-callback', function (req, res) {
  var querystring = Npm.require('querystring');
  console.log('Received POST /');

  // STEP 1: read POST data
  req.body = req.body || {};
  req.statusCode = 200;

  // read the IPN message sent from PayPal and prepend 'cmd=_notify-validate'
  var postreq = 'cmd=_notify-validate';
  for (var key in req.body) {
    if (req.body.hasOwnProperty(key)) {
      var value = querystring.escape(req.body[key]);
      postreq = postreq + "&" + key + "=" + value;

  // Step 2: POST IPN data back to PayPal to validate
  console.log('Posting back to paypal');'', { 
    params: postreq 
  }, function (error, result) {
    console.log('Error: ' + error);
    return console.log('Result: ', + result.content);
  return [204, 'No Content'];
}, {where: 'server'});

I get the initial IPN, but after my attempt to POST back, nothing.

I don’t get any errors and and I don’t get a result either. I’m using the IPN simulator and targeting my my SSL enabled staging site on AWS.

Server logs:

[] Received POST /
[] Posting back to paypal
[] Error: null
[] Result: 0

Has anyone used the andzdroid/paypal-ipn library to verify the PayPal IPN with Meteor? It’s a native node library – which isn’t a problem unless you start calling async when you need sync.

I have a situation in which my verification passes, but I can’t do anything in the call back, like a mongo_collection.insert(…), because of fibers. I tried to wrap in a Meteor.wrapAsync but still have issues.

Before forking and Meteor-ing it, I’d like to find out if anyone else has solve this issue.

What am I doing wrong here? The call just hangs at the Transactions.sendRequest call (the last thing I see is ‘calling sendRequest now…’)

[have to scroll to get to all the code]

var https = Npm.require('https');
var qs = Npm.require('querystring');
Transactions = {};

var SANDBOX_URL = '';
var REGULAR_URL = '';

Transactions.verify = function (params, settings) {
  //Settings are optional, use default settings if not set
  if (typeof callback === 'undefined' && typeof settings === 'function') {
    callback = settings;
    settings = {
      'allow_sandbox': false

  if (typeof params === 'undefined') {
    process.nextTick(function () {
      callback(new Error('No params were passed to ipn.verify'));

  params.cmd = '_notify-validate';

  var body = qs.stringify(params);

  //Set up the request to paypal
  var req_options = {
    host: (params.test_ipn) ? SANDBOX_URL : REGULAR_URL,
    method: 'POST',
    path: '/cgi-bin/webscr',
    headers: {'Content-Length': body.length}

  if (params.test_ipn && !settings.allow_sandbox) {
    process.nextTick(function () {
      callback(new Error('Received request with test_ipn parameter while sandbox is disabled'));
  console.log('calling sendRequest now...');
  var paypal_response = Transactions.sendRequest(req_options);

  return paypal_response;

Transactions.sendRequest = Meteor.wrapAsync(function (req_options) {
  var req = https.request(req_options, function paypal_request(res) {
    var data = [];
    console.log('calling res.on data now...');
    res.on('data', function paypal_response(d) {
    console.log('calling res.on end now...');
    res.on('end', function response_end() {
      var response = data.join('');

      //Check if IPN is valid
      if (response === 'VERIFIED') {
        callback(null, response);
      } else {
        callback(new Error('IPN Verification status: ' + response));

I’m trying to wrap the Transactions.sendRequest function in a Meteor.wrapAsync function because I need to write the output of the response to a collection. If I call the collection in a callback I get a Meteor Fiber error.

Just a few quick notes and hints here since I don’t have time right now to post a more complete guide about implementing PayPal in a Meteor app:

I’ve integrated with PayPal just a week ago and it definitely works. What I basically did was:

Add meteorhacks:npm for usage of npm modules.
Use PayPal’s node sdk, i.e. packages.json should contain:

{"paypal-rest-sdk": "1.5.2"}

Create a config file e.g. in server/lib:

paypal = Meteor.npmRequire('paypal-rest-sdk');
    mode: 'sandbox', //sandbox or live
    client_id: 'yourclientid',
    client_secret: 'yoursecret'

Use methods paypal.payment.create and paypal.payment.execute for communication with the PayPal API, something like this:

var ppCreate = Meteor.wrapAsync(paypal.payment.create.bind(paypal.payment));
var ppExecute = Meteor.wrapAsync(paypal.payment.execute.bind(paypal.payment));

Then you can use both of these methods something like this:

var response = ppCreate( ... paypal json/object just like in the SDK docs and samples ...);
if (resp.state !== 'created') {
    ... error handling ...

And then using iron:router create a URL that PayPal can redirect the user to after successful authorization of the payment (or cancellation):

Router.route('/payment/paypal/callback/:action', {
    name: 'paymentPaypalCallback'

Which reflects the paths you submitted to the PayPal API in the request json:

redirect_urls: {
        return_url: Meteor.absoluteUrl('payment/paypal/callback/execute', {replaceLocalhost: true}),
        cancel_url: Meteor.absoluteUrl('payment/paypal/callback/cancel', {replaceLocalhost: true})

The replaceLocahost: true is important for trying things out with the app running on localhost; without that setting PayPal won’t redirect the user back to your app since it will accept but not localhost (at least that’s how I understand it).

What PayPal will do is not call an additional route on your server, but simply redirect the user with enough additional params that you can properly identify and verify, and then execute that payment.

Is that clear enough and does that help?

I initially also considered using the meteor-paypal package, but I looked at the source and found it to be so simple and thought I could write my own code in a way that suited me and the application better, and so I did, and it was the right choice. That package also just used the paypal-rest-sdk npm module and provider a very thin wrapper around it.


This is great @seeekr, thanks for sharing!

I think you would provide a great service to the Meteor community by posting a more complete guide about implementing PayPal in a Meteor app when you have a few cycles!

I’ll review your advice and provide feedback when I come back around to flushing out the PayPal implementation more.

Thanks again –

@seeekr Would provide such a value if you could wrap it up in the paypal guide.
I wanted to use Braintree, Paymill or Stripe instead of PayPal, but from what I found you have to be real company to use these. Since I just want to try simple idea out to see if people would use it, it doesn’t make sense for me to found a company first.

Alright, I’ve put it on my TODO list now, should come around to that shortly and will post a link in here then!

1 Like

@mhlavacka you don’t need to be a company to use Stripe.

@seeekr, looking fordward to that article / guide too here!! :grin:

1 Like

I hope I can look at your paypal guide soon. Might need it in the very near future :smile:

I hope the link would be up already :3