How to build an appointment reminder SaaS app with Moola, Twilio and Stripe

2019-03-12

Let’s imagine you’re developing a SaaS app that allows dentists to send appointment reminders to their patients. You’ve hooked up your code to the appointment calendar (maybe using Nylas or Cronofy), and you’ve set up your integration with Twilio which will send an SMS to each patient the day before their appointment, like this:

Message from Dentist

You’re almost ready to launch, but you need to make sure your users don’t overuse your product by sending more SMS messages than they have paid for. You decide you want your customers to top up their account with credit (for example, via a Stripe payment), and you’ll charge them for every SMS they send.

Moola can help you to achieve this functionality very quickly, with just a few lines of code in your app, and without adding any more tables to your database.

Getting started

Head over to moola.dev and register an account, then set up an application and grab your API Key.

It will look something like this: c91edcf6-1716-4417-831a-1da2aa8e3ce5

Using Moola in your code

Make your API Key accessible to your application, for example by setting it to your environment variables. If you’re running on Heroku, for example, you could use the CLI command

1
heroku config:set MOOLA_API_KEY=c91edcf6-1716-4417-831a-1da2aa8e3ce5

Interacting with the Moola API is as simple as sending HTTP requests, and you can use your favourite library for this. For the examples in this post, we’re using Axios, a Promise-based HTTP client for Node.js.

A nice clean way to make it easy for Moola to be called from various places in your application’s code would be to create a helper file like moola.js, for example:

1
2
3
4
5
6
7
8
const axios = require("axios");

const moolaApi = axios.create({
baseURL: "https://api.moola.dev/v1",
headers: { "API-Key": process.env.MOOLA_API_KEY }
});

module.exports = moolaApi;

For more information about authentication with the Moola API, check the docs.

Setting up your customers

First of all, let’s set up a wallet for each of your customers. Your code will look something like the following example, which might be called from a controller or a model hook from your ORM.

In this example, we’ll set up a holder (your customer) and a wallet for them.

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
37
38
39
40
41
42
43
44
45
// you got a new customer! let's create a wallet for them
function onCustomerCreation(customer) {
return new Promise((resolve, reject) => {
// first we'll create a holder - the owner of the wallet
const holder = {
name: customer.name // e.g. John Smith
};

let holderId, walletId;

moolaApi
.post("/holders", holder)
.then(response => {
holderId = response.data.id;

// now let's create their wallet
const wallet = {
name: "Appointment reminder balance",
currency: "gbp", // British pounds sterling
balance: 100, // a free gift, perhaps?
balanceCanBeNegative: false, // prevent overspending
holderId: holderId
};

return balancedApi.post("/wallets", wallet);
})
.then(response => {
walletId = response.data.id;

// now we can save the info to your database with your customer record
customer.moolaWalletId = walletId;
customer.moolaHolderId = holderId;

return customer.save();
})
.then(customer => {
// all done!
return resolve(customer);
})
.catch(err => {
// oops - an error occurred
return reject(err);
});
});
}

Top-ups for your customers

When your customer wants to add funds to their balance, just send Moola information about how much they paid. For example, here’s some code that you could use when handling a Stripe webhook after a successful charge.

For more information about transactions, check out the docs.

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
// your customer just paid some money via Stripe
function onTopUpPayment(payment, customer) {
return new Promise((resolve, reject) => {
// create a transaction for this customer's wallet
const transaction = {
walletId: customer.moolaWalletId,
amount: payment.amount, // e.g. 1000 for 10 GBP
type: "credit",
description: "Top up via Stripe",
reference: payment.id // useful for cross-referencing of records
};

moolaApi
.post("/transactions", transaction)
.then(response => {
const transactionId = response.data.id;

// you could do something else with the transaction here
return resolve(response);
})
.catch(err => {
// oops - an error occurred
return reject(err);
});
});
}

Charging your customers for their usage

When your customers send SMS appointment reminders, you want to bill them for the usage. You can use transactions again, this time debiting the customer’s wallet. For example:

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
// you're about to send an appointment reminder for your customer
function onReminderCreation(reminder, customer) {
return new Promise((resolve, reject) => {
// create a transaction for this customer's wallet
const transaction = {
walletId: customer.moolaWalletId,
amount: reminder.cost, // e.g. 10 for GBP 0.10 (ten pence)
type: "debit",
description: `Send appointment reminder to ${reminder.recipient}`,
reference: reminder.id // useful for cross-referencing records
};

moolaApi
.post("/transactions", transaction)
.then(response => {
// save the transaction ID against the reminder
reminder.moolaTransactionId = response.data.id;
return reminder.save();
})
.then(reminder => {
// all done!
return resolve(reminder);
})
.catch(err => {
// oops - an error occurred
return reject(err);
});
});
}

Preventing further usage after balance runs out

One of the handy things about Moola is that it can provide some basic rules and controls around wallet balances for you. For instance, since we specified earlier that this wallet must not have a negative balance (using the balanceCanBeNegative parameter), any attempts to create transactions for this wallet which would cause the balance to fall below zero will fail. By handling this case in our app’s logic, we can make sure customers can only spend what they’ve paid for.

Let’s add some code to the error handler (.catch()) from our onReminderCreation() function to demonstrate this. For more information about error codes, check out the docs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// this is the .catch handler in our onReminderCreation() function
.catch(err => {
// let's check if the error is about the wallet's balance
if (
err.response.data &&
err.response.data.code === "balance_insufficient"
) {
const error = new Error("Your balance is too low, please top up!");
return reject(error);
} else {
// oops - there was a different error
return reject(err);
}
});

Showing your customers their usage history

You might want to show your users their balance history, so they can see how much they have spent, and when. This can be done like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// you're about to get all the transactions for a specific customer's wallet
function getWalletTransactions(customer) {
return new Promise((resolve, reject) => {
moolaApi
.get(`/transactions?walletId=${customer.moolaWalletId}`)
.then(response => {
// all done!
return resolve(response.data);
})
.catch(err => {
// oops - an error occurred
return reject(err);
});
});
}

Wondering about pagination? Check out the docs.

Similarly, you can get information about a single transaction for a particular reminder, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// get details about a specific transaction
function getReminderTransaction(reminder) {
return new Promise((resolve, reject) => {
moolaApi
.get(`/transactions/${reminder.moolaTransactionId}`)
.then(response => {
// all done!
return resolve(response.data);
})
.catch(err => {
// oops - an error occurred
return reject(err);
});
});
}

Launch and smile

Your app is ready to launch - just think of all the clean, shiny teeth your users will have 😁.

Shiny teeth

Try it yourself

Moola is an API for the accounting parts of your application, with flexible pricing plans to meet your business needs. We handle the money plumbing like wallets, transactions and balances for you, so you can build your application quicker and focus on delighting your users.

Sign up for a free account or contact us to learn more. We can’t wait to see what you build.