Update 2/29/16: These code examples have been updated to reflect the 3.0 release of the express-stormpath integration.

Stormpath provides authentication tools for APIs, so we work closely with devs building new REST services. We also hear a lot about the challenges that come with building an API. Billing is often high on that list of pitfalls. While charging users has long been a complicated issue, it can also be surprisingly painless for many use cases. We’ll show you how!

In this tutorial, I’ll run through how to:

Prop up a simple web console where your API users can register, login, get a Key for your REST API, and update their Account to a paid plan

Setup a monthly subscription plan and email users with monthly invoices

Securely collect credit card data and charge a recurring fee

Store unique billing info on your user records

Expose a simple REST endpoint, secured with HTTP basic authentication

Limit access to that endpoint to paying users only

Revoke API access when a user unsubscribes or fails to pay an invoice

This tutorial should take less than half an hour to run through. We’ll use Stormpath for user management and API Key Management and Stripe for all things billing related. We’re also running Node.js + Express.js, but the steps are the same no matter your stack. Leave a comment or email [email protected] if you have questions on using this guide in your app.

Scaffolding for this project is based on an earlier blog on writing a full-fledged API service. It’s a great resource for in-depth code explanations, as I’ll focus mostly on billing aspects here.

Set Up Your User Store – Stormpath

To get started, register for a free Stormpath developer account. Stormpath is an authentication and user management service that stores your user accounts and exposes a host of endpoints for working with those users. Sign up here, click the verification link in your Email and login into the admin console here.

Once you’re in, download your Stormpath API Key (located under the “Developer Tools” section on the homepage). You will need the values in the apiKey.properties file to test your work.

Next, create a Stormpath Application to represent this sample project. Stormpath Applications are convenience resources to help you model out your user data; create one for every real-world app backed by Stormpath. Go ahead and click on “Applications” in the navigation bar, hit the blue “Create Application” button and keep the settings default in the subsequent popup. Name the Application whatever you want, but something like “Sample Billing API” would work nicely! Once created, take note of the Application’s REST URL because we’ll need it too.

As a last step, let’s turn on email verification. To do so, click the “Directories” tab and find the Directory auto-generated for the new Application (e.g. “Sample Billing API Directory”). Once on the page for this Directory, click the “Workflows” link on the left-hand sidebar and you will find yourself looking at the Email verification workflow page.

Make three changes on this page: Update the drop down value to “enabled”, set the “Link Base URL” to http://localhost:3000/verified and click the blue “Save Changes” button. Optionally, update the wording of the email to whatever you like, so long as you include the ${url} macro.

Now that this workflow is enabled, all new Accounts will be created with an unverified status in Stormpath and will not be allowed to authenticate until they click the email sent to them on registration. This type of verification step is generally just a good practice for any sort of secure app, but it’s a requirement for us because we are going to use the email address users give us on registration to create a customer record for them. Knowing that every user actually has access to the email address they register with is therefore that much more important.

Setup Your Billing Provider – Stripe

We have one more service provider to register for: Stripe. Stripe manages credit card data, subscriptions and payment transactions, so we don’t have to worry about things like PCI compliance, or building a billing backend.

Once registered, you’ll notice that your Stripe Account is set to “test”. Leave that setting alone as it will allow us to use test credit cards when we’re ready.

Hold off on further Stripe configurations for now; just make sure to take note of your Stripe API Keys. More specifically, your pair of test Keys. You can find them in your account page under the “API Keys” tab.

Write the Web Console and REST API

All of the code for this project is available on GitHub. To follow along directly, pull down the repo and cd into the project folder. Once downloaded, install the Node.js dependencies by running npm install from your terminal which will automatically pull what you need from the package.json file. Assuming you have Node.js and NPM installed of course =).

Next, run bower install to get the frontend dependencies via the bower.json file.

Our basic web console should have a few key functions right away:

It can securely register users with a username and password securely It can consume verification tokens to enable newly created users after they click through the verification email It can log in users to a basic dashboard page and create a secure session

The core functionality of our app is wrapped up in the index.js file. Here, we import our libraries and routes:

var async = require('async'); var express = require('express'); var stormpath = require('express-stormpath'); var apiRoutes = require('./routes/api'); var privateRoutes = require('./routes/private'); var publicRoutes = require('./routes/public'); 1 2 3 4 5 6 7 8 var async = require ( 'async' ) ; var express = require ( 'express' ) ; var stormpath = require ( 'express-stormpath' ) ; var apiRoutes = require ( './routes/api' ) ; var privateRoutes = require ( './routes/private' ) ; var publicRoutes = require ( './routes/public' ) ;

Create the Express.js application:

var app = express(); 1 2 var app = express ( ) ;

Specify a templating engine:

app.set('view engine', 'jade'); app.set('views', './views'); 1 2 3 app . set ( 'view engine' , 'jade' ) ; app . set ( 'views' , './views' ) ;

Configure API access to Stripe:

app.locals.stripePublishableKey = process.env.STRIPE_PUBLISHABLE_KEY; 1 2 app . locals . stripePublishableKey = process . env . STRIPE_PUBLISHABLE_KEY ;

Configure middleware to serve static files:

app.use('/static', express.static('./static', { index: false, redirect: false })); app.use('/static', express.static('./bower_components', { index: false, redirect: false })); 1 2 3 4 5 6 7 8 9 app . use ( '/static' , express . static ( './static' , { index : false , redirect : false } ) ) ; app . use ( '/static' , express . static ( './bower_components' , { index : false , redirect : false } ) ) ;

Configure Stormpath’s Express.js integration:

app.use(stormpath.init(app, { expand: { apiKeys: true, customData: true }, web:{ login: { nextUri: '/dashboard' } }, postRegistrationHandler: function(account, req, res, next) { async.parallel([ // Create an API key for this user. function(cb) { account.createApiKey(function(err, key) { if (err) return cb(err); cb(); }); } ], function(err) { if (err) return next(err); next(); }); } })); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 app . use ( stormpath . init ( app , { expand : { apiKeys : true , customData : true } , web : { login : { nextUri : '/dashboard' } } , postRegistrationHandler : function ( account , req , res , next ) { async . parallel ( [ // Create an API key for this user. function ( cb ) { account . createApiKey ( function ( err , key ) { if ( err ) return cb ( err ) ; cb ( ) ; } ) ; } ] , function ( err ) { if ( err ) return next ( err ) ; next ( ) ; } ) ; } } ) ) ;

Specify route code:

app.use('/', publicRoutes); app.use('/api', stormpath.apiAuthenticationRequired, apiRoutes); app.use('/dashboard', stormpath.loginRequired, privateRoutes); 1 2 3 4 app . use ( '/' , publicRoutes ) ; app . use ( '/api' , stormpath . apiAuthenticationRequired , apiRoutes ) ; app . use ( '/dashboard' , stormpath . loginRequired , privateRoutes ) ;

And finally, prop up our server.

app.listen(process.env.PORT || 3000); 1 2 app . listen ( process . env . PORT | | 3000 ) ;

To illustrate further, let’s take a quick look at the views routes.

public.js

'use strict'; var express = require('express'); var stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); // Globals var router = express.Router(); // Routes router.get('/', function(req, res) { res.render('index'); }); router.get('/pricing', function(req, res) { res.render('pricing'); }); // Exports module.exports = router; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 'use strict' ; var express = require ( 'express' ) ; var stripe = require ( 'stripe' ) ( process . env . STRIPE_SECRET_KEY ) ; // Globals var router = express . Router ( ) ; // Routes router . get ( '/' , function ( req , res ) { res . render ( 'index' ) ; } ) ; router . get ( '/pricing' , function ( req , res ) { res . render ( 'pricing' ) ; } ) ; // Exports module . exports = router ;

As you can see, there are only two public pages we absolutely need: A homepage for our app and a pricing page so we can tell new users how much API access will cost them. And why it’s totally worth the cost.

private.js

'use strict'; var bodyParser = require('body-parser'); var express = require('express'); var stormpath = require('express-stormpath'); var stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); // Globals var router = express.Router(); // Middlewares router.use(bodyParser.urlencoded({ extended: true })); // Routes router.get('/', function(req, res) { res.render('dashboard'); }); // Exports module.exports = router; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 'use strict' ; var bodyParser = require ( 'body-parser' ) ; var express = require ( 'express' ) ; var stormpath = require ( 'express-stormpath' ) ; var stripe = require ( 'stripe' ) ( process . env . STRIPE_SECRET_KEY ) ; // Globals var router = express . Router ( ) ; // Middlewares router . use ( bodyParser . urlencoded ( { extended : true } ) ) ; // Routes router . get ( '/' , function ( req , res ) { res . render ( 'dashboard' ) ; } ) ; // Exports module . exports = router ;

For now, private.js just needs to serve up our dashboard.jade template. You may be wondering why there are no routes related to auth. The answer is Express-Stormpath takes care of all the basic authentication functionality (including views) like registration, login, and email verification, all out of the box.

However, to make Express-Stormpath work the way we want it to, there are three important configurations to set in index.js:

First, set enableAccountVerification to true so the library knows to expect an enabled verification workflow. Second, tell Express-Stormpath to redirect to /dashboard after registration and login. Lastly, pass in a long, randomly-generated secret to encrypt sessions.

We’re just missing one key piece of functionality… our money maker! Inside the routes directory, add one more file: api.js . My API has just one endpoint, /hi that greets API consumers with a friendly message.

router.post('/hi', function(req, res) { res.status(200).json({ hi: 'there' }); }); 1 2 3 4 router . post ( '/hi' , function ( req , res ) { res . status ( 200 ) . json ( { hi : 'there' } ) ; } ) ;

Generate API Keys for Your Users with Stormpath

If you plan to charge for your API, you also want to secure it with proper authentication. This means username and password aren’t going to cut it and your app needs to generate a unique set of high entropy API Keys for each user, just as Stripe and Stormpath did when we registered earlier.

Express-Stormpath can do this step automatically on every registration by defining a custom [post registration handler]http://docs.stormpath.com/nodejs/express/latest/registration.html#post-registration-handler):

app.use(stormpath.init(app, { expand: { apiKeys: true, customData: true }, web: { login: { nextUri: '/dashboard' }, }, postRegistrationHandler: function(account, req, res, next) { async.parallel([ // Create an API key for this user. function(cb) { account.createApiKey(function(err, key) { if (err) return cb(err); cb(); }); } ], function(err) { if (err) return next(err); next(); }); } })); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 app . use ( stormpath . init ( app , { expand : { apiKeys : true , customData : true } , web : { login : { nextUri : '/dashboard' } , } , postRegistrationHandler : function ( account , req , res , next ) { async . parallel ( [ // Create an API key for this user. function ( cb ) { account . createApiKey ( function ( err , key ) { if ( err ) return cb ( err ) ; cb ( ) ; } ) ; } ] , function ( err ) { if ( err ) return next ( err ) ; next ( ) ; } ) ; } } ) ) ;

Once the keys are generated, it’s a quick job to expose them to the user in the dashboard. Add a section to display the API Key and ID like so:

dashboard.jade .row.api-keys ul.list-group .col-xs-offset-1.col-xs-10 li.list-group-item.api-key-container .left strong API Key ID: span.api-key-id #{user.apiKeys.items[0].id} .right strong API Key Secret: span.api-key-secret #{user.apiKeys.items[0].secret} 1 2 3 4 5 6 7 8 9 10 11 12 dashboard . jade . row . api - keys ul . list - group . col - xs - offset - 1.col - xs - 10 li . list - group - item . api - key - container . left strong API Key ID : span . api - key - id #{user.apiKeys.items[0].id} . right strong API Key Secret : span . api - key - secret #{user.apiKeys.items[0].secret}

And there you have it! Your users can register, verify they are who they say they are, find their API credentials and use them to hit your awesome new REST endpoint.

Add Billing to Your API

Return to the Stripe dashboard to continue setting up your account, starting by creating a new plan. This is where you get to determine what a subscription to your API looks like. Here’s mine for reference, but be sure to play with the details!

For this sample, I only need one plan (only one endpoint after all), but it’s entirely possible to create more.

At this point, I want to briefly acknowledge that monthly subscriptions are far from the only billing model out there. They are simply what most of our users here at Stormpath are implementing and mesh with the overall trend to a SaaS-based world. Still, a better option for some APIs is going to be a charge-per-query model as seen here. Fortunately for all of us, Stripe supports both.

Now that the plan is ready in Stripe, add a form to your own dashboard to collect a user’s credit card data and POST it to Stripe. The easiest way to do that is with Stripe checkout. Here’s how that might look in our dashboard template:

.row.widgets .col-md-offset-4.col-md-4 .panel.panel-primary .panel-heading.text-center h3.panel-title Billing .billing-content.text-center span h3. Upgrade To Pro form(action='/dashboard/charge', method='POST') script.stripe-button( src = 'https://checkout.stripe.com/checkout.js', data-email = '#{user.email}', data-key = '#{stripePublishableKey}', data-name = '#{siteTitle}', data-amount = '1000', data-allow-remember-me = 'false' ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 . row . widgets . col - md - offset - 4.col - md - 4 . panel . panel - primary . panel - heading . text - center h3 . panel - title Billing . billing - content . text - center span h3 . Upgrade To Pro form ( action = '/dashboard/charge' , method = 'POST' ) script . stripe - button ( src = 'https://checkout.stripe.com/checkout.js' , data - email = '#{user.email}' , data - key = '#{stripePublishableKey}' , data - name = '#{siteTitle}' , data - amount = '1000' , data - allow - remember - me = 'false' )

However, that’s actually only half the battle. Because this form lives on the client side, what it actually does is create a token. This token is then passed to a private route ( /charge ) that will POST it to Stripe with instructions on what sort of action we want to take.

In our case, we want to do three things:

Create the customer in Stripe and add them to our Plan In a callback, save the user’s new Stripe customer ID to their Stormpath Account record Save information about the plan to the user’s Stormpath Account record

At a high level, the function uses the session Express-Stormpath created ( req.user ) on authentication to find and update the correct Account. More specifically, it’s saving data (from Stripe) to the Account’s customData ; a schemaless JSON resource available on all Stormpath Accounts. customData can store whatever user data you want in Stormpath and that means we don’t have to spin up a database =).

router.post('/charge', function(req, res, next) { stripe.customers.create({ source: req.body.stripeToken, plan: 'pro', email: req.user.email }, function(err, customer) { if (err) return next(err); // Add the user to this group. req.app.get('stormpathApplication').getGroups({ name: 'pro' }, function(err, groups) { if (err) return next(err); var group = groups.items[0]; req.user.addToGroup(group, function(err) { if (err) return next(err); // Update the user's plan. req.user.customData.billingTier = customer.subscriptions.data[0].plan; req.user.customData.billingProviderId = customer.id; req.user.customData.save(function(err) { if (err) return next(err); res.redirect('/dashboard'); }); }); }); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 router . post ( '/charge' , function ( req , res , next ) { stripe . customers . create ( { source : req . body . stripeToken , plan : 'pro' , email : req . user . email } , function ( err , customer ) { if ( err ) return next ( err ) ; // Add the user to this group. req . app . get ( 'stormpathApplication' ) . getGroups ( { name : 'pro' } , function ( err , groups ) { if ( err ) return next ( err ) ; var group = groups . items [ 0 ] ; req . user . addToGroup ( group , function ( err ) { if ( err ) return next ( err ) ; // Update the user's plan. req . user . customData . billingTier = customer . subscriptions . data [ 0 ] . plan ; req . user . customData . billingProviderId = customer . id ; req . user . customData . save ( function ( err ) { if ( err ) return next ( err ) ; res . redirect ( '/dashboard' ) ; } ) ; } ) ; } ) ; } ) ; } ) ;

I chose to call the two new customData keys billingProviderId and billingTier , but you can use whatever JSON compatible values you like.

Implement Authorization in Your API

At this point, the user can register, connect to your API securely, and pay you. However, there’s one more thing to do: restrict access to the API to paying users only. We can’t be greeting freeloaders after all!

Commonly referred to as authorization, the API needs to check who the caller is, what plan they are on and whether they should have access to the endpoint. The first element, knowing who they are, has already been implemented via HTTP Basic Auth.

With the user identified, verify that their plan matches what it should. Here’s our basic authorization check on the updated api/hi route:

router.post('/hi', function(req, res) { if (!req.user.customData.billingTier || req.user.customData.billingTier.id !== 'pro') { res.status(402).json({ error: 'Please Upgrade to the pro plan' }); } else { res.status(200).json({ hi: 'there' }); } }); 1 2 3 4 5 6 7 8 router . post ( '/hi' , function ( req , res ) { if ( ! req . user . customData . billingTier | | req . user . customData . billingTier . id ! == 'pro' ) { res . status ( 402 ) . json ( { error : 'Please Upgrade to the pro plan' } ) ; } else { res . status ( 200 ) . json ( { hi : 'there' } ) ; } } ) ;

In a production app, you would want to decouple the authorization check into middleware, but hopefully this helps illustrate how simple the logic is. api/hi is officially available to paid users only.

Run Your API Service – with Billing!

Of course, you’ll want to check that everything is working as expected! Remember all those Stripe and Stormpath credentials you collected in the beginning? Now is the time to expose them to your application as environment variables.

export STRIPE_PUBLISHABLE_KEY=StripeTestPublishableKeyGoesHere export STRIPE_SECRET_KEY= StripeTestSecretKeyGoesHere export STORMPATH_CLIENT_APIKEY_ID=StormpathAPIKeyIDGoesHere export STORMPATH_CLIENT_APIKEY_SECRET= StormpathAPIKeySecretGoesHere 1 2 3 4 5 export STRIPE_PUBLISHABLE_KEY = StripeTestPublishableKeyGoesHere export STRIPE_SECRET_KEY = StripeTestSecretKeyGoesHere export STORMPATH_CLIENT_APIKEY_ID = StormpathAPIKeyIDGoesHere export STORMPATH_CLIENT_APIKEY_SECRET = StormpathAPIKeySecretGoesHere

Run the application with: node index.js and visit the index page in your browser at http://localhost:3000 where you should be greeted with:

You can now check out the pricing page, be thoroughly convinced, and register for the app. Once logged in, you should see a set of API credentials for your API. Use these to make a test request against your api with cURL:

curl -v —user 'apiKeyID:apiKeySecret' -H 'Content-Type: application/json' 'http://127.0.0.1:3000/api/hi' 1 2 curl - v — user 'apiKeyID:apiKeySecret' - H 'Content-Type: application/json' 'http://127.0.0.1:3000/api/hi'

If all is well, your API should return a HTTP 402 error response with a message telling you to upgrade.

Go back into the dashboard and click the Upgrade button. Because Stripe is in test mode, use 4242 4242 4242 4242 for the card number, any future date for the expiration field and a random 3 digits for the cvc.

To verify that the transaction went through, try running the exact same cURL command again. Congratulations! You now have a fully functional web console and REST API with billing built-in and enforced.

Optional Configurations

There are a nearly unlimited number of things you could do to improve this rather paltry API. Here are three to consider.

Revoke Access When a User Fails to Pay

Once your service blows up in popularity, it will become increasingly annoying to manually update every Account that stops paying. Stripe webhooks are a great way to automate this process. In our case, we want to setup a webhook that fires off whenever a customer’s subscription is deleted.

Once the webhook is configured in Stripe, expose a public route to consume the event. Due to the nature of webhooks, we can’t simply trust that Stripe was the one to hit our endpoint so there are a few additional steps we need to take to be on the secure side:

Consume the webhook from Stripe and parse out the event ID POST back to Stripe using the event ID and check that the event matches the type we expect Retrieve the customer associated with the event and parse out their Email Search Stormpath for the Account associated with that Email address Update the Account to reflect their new subscription status Respond to Stripe to indicate the webhook was successfully received

Here’s how that looks:

router.post('/subscription-cancel', function(req, res, next) { stripe.events.retrieve(req.body.id, function(err, event) { if (err) return next(err); var type = event.type; // Check that the event type is a subscription cancellation. if (type !== 'customer.subscription.deleted') { return res.json(); } var customerId = event.data.object.customer; stripe.customers.retrieve(customerId, function(err, customer) { if (err) return next(err); var customerEmail = customer.email; req.app.get('stormpathApplication').getAccounts({ email: customerEmail }, function(err, accounts) { if (err) return next(err); var account = accounts.items[0]; account.getCustomData(function(err, data) { if (err) return next(err); data.billingTier.id = 'cancelled'; data.save(function(err) { if (err) return next(err); return res.json(); }); }); }); }); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 router . post ( '/subscription-cancel' , function ( req , res , next ) { stripe . events . retrieve ( req . body . id , function ( err , event ) { if ( err ) return next ( err ) ; var type = event . type ; // Check that the event type is a subscription cancellation. if ( type ! == 'customer.subscription.deleted' ) { return res . json ( ) ; } var customerId = event . data . object . customer ; stripe . customers . retrieve ( customerId , function ( err , customer ) { if ( err ) return next ( err ) ; var customerEmail = customer . email ; req . app . get ( 'stormpathApplication' ) . getAccounts ( { email : customerEmail } , function ( err , accounts ) { if ( err ) return next ( err ) ; var account = accounts . items [ 0 ] ; account . getCustomData ( function ( err , data ) { if ( err ) return next ( err ) ; data . billingTier . id = 'cancelled' ; data . save ( function ( err ) { if ( err ) return next ( err ) ; return res . json ( ) ; } ) ; } ) ; } ) ; } ) ; } ) ; } ) ;

To test, expose your local server to the internet so Stripe’s webhook can hit the new route. Ngrok is a great option for that. Once running, update the Stripe webhook to point at your public ngrok URL and cancel a test customer’s subscription. If successful, you should see an update on their Stormpath Account’s customData to reflect the cancellation.

Configure Stripe to Send Invoice Receipts via Email

This may not seem like a big deal, but trust us, it is super convenient for you and your customers! The Stormpath billing team fully endorses this option =). Enable it in Stripe’s Account Settings Email tab.

Add Paid Users to a Stormpath Group

I chose to use customData for authorization in my example because its flexibility would allow me to implement very granular authorization rules based on the plan data collected from Stripe. However, Stormpath does support the notion of a Group that’s more commonly used used for authorization. The Group approach is handy because it is very simple to query against Stormpath for all users that belong to a particular Group.

To get the best of both worlds, create a new Group to represent the Stripe plan in Stormpath and add users to it when they upgrade. To create the Group, log into the Stormpath admin console, find your Directory, Click ‘Groups’ in the sidebar and click the ‘Create Group’ button.

Now update /charge to additionally add the user to a Group:

router.post('/charge', function(req, res, next) { stripe.customers.create({ source: req.body.stripeToken, plan: 'pro', email: req.user.email }, function(err, customer) { if (err) return next(err); // Add the user to this group. req.app.get('stormpathApplication').getGroups({ name: 'pro' }, function(err, groups) { if (err) return next(err); var group = groups.items[0]; req.user.addToGroup(group, function(err) { if (err) return next(err); // Update the user's plan. req.user.customData.billingTier = customer.subscriptions.data[0].plan; req.user.customData.billingProviderId = customer.id; req.user.customData.save(function(err) { if (err) return next(err); res.redirect('/dashboard'); }); }); }); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 router . post ( '/charge' , function ( req , res , next ) { stripe . customers . create ( { source : req . body . stripeToken , plan : 'pro' , email : req . user . email } , function ( err , customer ) { if ( err ) return next ( err ) ; // Add the user to this group. req . app . get ( 'stormpathApplication' ) . getGroups ( { name : 'pro' } , function ( err , groups ) { if ( err ) return next ( err ) ; var group = groups . items [ 0 ] ; req . user . addToGroup ( group , function ( err ) { if ( err ) return next ( err ) ; // Update the user's plan. req . user . customData . billingTier = customer . subscriptions . data [ 0 ] . plan ; req . user . customData . billingProviderId = customer . id ; req . user . customData . save ( function ( err ) { if ( err ) return next ( err ) ; res . redirect ( '/dashboard' ) ; } ) ; } ) ; } ) ; } ) ; } ) ;

Other Resources on API Authentication

And that’s a wrap! Feedback and questions are most welcome in the comments, and you can always email [email protected] for answers and assistance.