Guide

The following walks through step-by-step the high-level of what would be required to implement a Recurr Endpoint on your service. Pseudo-code examples are provided in NodeJS using Mongoose (MongoDB), but should be easily adaptable to other languages.

Generating a unique Recurr Token for each user

The first step to implementing Recurr is to create a new database column to store a Recurr ID (or "Token" as it should be revocable in case of compromise) for each user. For example, assuming we have a `users` table, we might name this field `recurrToken`. Our users table may then contain the following columns:

Users
idemail...recurrToken
1john.smith@email.com...9586f67464963a6ed4c6d6b7fababe23
............

A migration should be performed to set the `recurrToken` on existing users and should be automatically generated upon registration for new users.

The ID itself should be a randomly generated string with sufficient entropy (16 bytes/128 bits recommended) to prevent brute-force attacks against the URL. This could be generated as a random UUID or using a custom function. An example of how this might be done in NodeJS using the built-in crypto module is below.

const token = require('crypto').randomBytes(16).toString('hex') // 9586f67464963a6ed4c6d6b7fababe23

Adding Recurr Endpoint

The Recurr Enpoint is the URL that users will scan into a Recurr-compatible App. There is no naming convention for this endpoint - the service can use whatever it determines to best fit in with their architecture. However, it must be a GET request and it must support CORS in order to support web-based clients.

An example endpoint for a user with "recurrToken" of 9586f67464963a6ed4c6d6b7fababe23 might look as follows:

https://your-service.com/feed/9586f67464963a6ed4c6d6b7fababe23

Defining this endpoint in ExpressJS (a NodeJS web framework) may look as follows (note that CORS has been added as a middleware to support Web-based Recurr clients):

app.get(‘/feed/:recurrToken’, cors(), function(req, res) { res.send(“OK”) }

Generating Feed Content for User

Now that we have an endpoint, we want to generate the Recurr Feed for the user with the given Recurr Token.

In this example, we will implement the body of the above endpoint to retrieve all bills and receipts for a given user.

We're going to assume there's a "bills" table/collection that contains the following fields:

Bills
idupdatedAtuserIdtitleduetotalpaid
12021-03-29T07:12:25.613Z1March 20202021-03-30T07:12:25.613Z49.992021-03-29T07:12:25.613Z
22021-04-10T07:12:25.613Z1April 20202021-04-30T07:12:25.613Z49.99
.....................

The feed generation code for this may then look as follows:

app.get(‘/feed/:recurrToken’, cors(), async function(req, res) {
  try {
    // Retrieve user based on recurrToken param
    const user = await User.findOne({ recurrToken: req.params.recurrToken })

    // If user is not found, return 404 status code
    if (!user) {
      return res.status(404).send('Feed does not exist')
    }

    // Generate the header of our feed
    const feed = {
      "protocol": "recurr",
      "version": 1.0,
      "title": "Your Service",
      "caption": user.email,
      "icon": "https://recurr.app/examples/icons/recurr.svg",
      "bills": {},
      "receipts": {}
    }

    // If Recurr client sends a delta param, let's only send new/updated receipts to save bandwith
    feed.mode = {
      bills: 'set',
      receipts: (req.query.delta) ? 'update' : 'set'
    }

    // Now let's pull a list of unpaid bills belonging to this user
    const bills = await Bill.find({
      userId: user.id,
      paid: false // Only retrieve unpaid bills
    })

    // Let's add each of these to the feed
    bills.forEach(bill => {
      feed.bills[bill.id] = {
        "title": bill.title, // E.g. "March 2021"
        "due": bill.due, // E.g. "2021-03-31T23:59:59.836Z",
        "total": bill.total, // E.g. 15.43
        "currency": "USD", // Curreny of total
        "actions": [
          { // Paypal Payment Option
            "icon": "https://recurr.app/examples/icons/paypal.svg", // Icon for Payment Option
            "title": "Pay with PayPal", // Title of Payment Option
            "url": `https://www.paypal.com/${bill.id}` // URL to execute when clicked
          },
          { // Bitcoin Cash Payment Option
            "icon": "https://recurr.app/examples/icons/bch.svg", // Icon for Payment Option
            "title": "Pay with BCH", // Title of Payment Option
            "url": `bitcoincash:qp8xazl7savalpzwm0tf7dq2an3cc0aj5vrwmmsd5n?amount=0.0005&label=${bill.id}` // URL to execute when clicked
          }
        ]
      }
    })

    // Now let's pull a list of receipts.
    const receipts = await Bill.find({
      userId: user.id,
      paid: true, // Only retrieve bills that are paid
      updatedAt: { $gt: req.query.delta || 0 }
    })

    // Let's add each of these to the feed
    receipts.forEach(receipt => {
      feed.receipts[receipt.id] = {
        "title": receipt.title, // E.g. "March 2021"
        "due": receipt.due, // E.g. "2021-03-31T23:59:59.836Z",
        "total": receipt.total, // E.g. 15.43
        "currency": "USD", // Currency of total
        "paid": receipt.paid, // E.g. true/false or an ISO8601 Date String (preferable)
        "actions": [
          {
            "icon": "https://recurr.app/examples/icons/receipt.svg", // Icon for Receipt
            "title": "View Receipt", // Title of action
            "url": `https://your-service.com/receipt/${receipt.id}` // URL to execute when clicked
          }
        ]
      }
    })

    // Send the feed
    return res.send(feed)
  } catch (err) {
    res.status(500).send(err.message)
  }
}