Getting started

Introduction

The Repeat Payments Network (RPN) makes collecting invoice payments easy by sending text message pay-links. The API is organized around REST and has predictable, resource-oriented URLs, and uses HTTP response codes to indicate API errors. We use built-in HTTP features, like HTTP authentication and HTTP verbs, which are understood by off-the-shelf HTTP clients. We support cross-origin resource sharing, allowing you to interact securely with our API from a client-side web application (though you should never expose your secret API key in any public website's client-side code). JSON is returned by all API responses, including errors, although our API libraries convert responses to appropriate language-specific objects. To make the API as explorable as possible, accounts have test mode and live mode API keys. Use the appropriate key to perform a live or test transaction. Additionally, you can also 'switch' the payment gateway between test and production modes. Requests made with the payment gateway in test mode never hit the payment processing networks and incur no charge. The RPN API is a wrapper around multiple payment gateways and compatible with all of the major payment processors, and enables customers to quickly pay for open invoices using their mobile wallet such as Apple Pay, or by inputting credit/debit card information.

There are three key parts to an integration with the API - you can do one, two or all three of them:

  • Generating short code pay link URL
  • Transmitting the pay link URL to customer through RPN messaging
  • Staying up-to-date with webhooks

To start, please contact us to obtain an application key. This key is unique to your application will have to be stored securely within your infrastructure. You will use the application key to obtain an access token that is unique to each merchant or MID. You need an access token, unique to each merchant, to call our APIs. There are two ways to obtain the access token for each merchant activation.

(1) Manually submit your application key from within each merchant's account dashboard. This will return the access token which you would then copy and store securely on your end along the MID. This integration is generally much faster of the two methods to obtain an access token.

(2) Store the merchant user/pwd login credentials, and use it to programatically obtain the access token. This requires integration of your application to Amazon's AWS identity infrastructure.

In this guide, we’ll get you up and running with the RPN API. You’ll add your first customer, create your first payment short code URL tied to your invoice, collect your first invoice payment, and set up webhooks to stay up to date on the status of your payment. The guide includes code samples in PHP, Ruby, Python, C# and Java.

Getting started

Creating Payment URL

To make it quick and easy for you to build your RPN integration, our web based APIs can be called from a variety of applications built in Java, Python, Ruby, PHP, and C# without writing lots of boilerplate code, Just copy and paste.

Use our simple RESTful API by building the HTTP requests yourself - see our API reference to get started.

Obtaining the application key

Please send email to developers@pymnt.us to request your application key

Creating an access token

To start using the API, you’ll need an access token.

First, sign up for an account in our sandbox: the sandbox is our dedicated testing environment where you can build and test your integration. During your testing, you can switch the payment gateway between test and production modes. In the test mode, you will avoid incurring real charges on your credit or debit card.

Next, create an developer. Head to https://sandbox.pymnt.us/account, after your verify your email address a developer account will be created for you, along with a test merchant account.

Next, complete the test merchant information, which you will use to test your app.

Next, set up your payment environment for your test merchant. Switch between production and test as appropriate.

Your payment processing partner can provide the specific details, including the API secret. For example, to generate secret on the Worldnet payment gateway.

  • Under 'Settings', go to 'Terminal'
  • Enter the identical Shared Secret into both the 'Secret' and 'Confirm Secret' fields
  • Update your settings and confirm you want to change the Shared Secret

Finally, in the 'Access Token' tab submit your 'Application key' to create the access token, and copy the token to your clipboard.

You can avoid the manual copy of the access token by programatically obtaining the access token for each merchant. In order to accomplish this, you will need to implement the AWS Cognito Identity Pool login. Go to https://docs.aws.amazon.com/cognito/latest/developerguide/amazon.html for details

Once you are logged into your account, the API below will allow you to retrieve the unique access_token for each merchant.

Request:

End Point:
Dev: https://api-sandbox.pymnt.us/accounts/access_token Method: GET
Sandbox: https://api-sandbox.pymnt.us/accounts//access_token Method: GET
Production: https://api.pymnt.us/accounts/access_token Method: GET

Headers:
Authorization :
x-api-key :
provider :< provider >

Response:
{
“ACCESS_TOKEN”:
}



Generate short code payment link

Short code API is a HTTP POST request with a JSON body. Api headers will contain access token authorization and x-api-key. The short code API expects the invoice number and the associated payment due amount, and optionally the customer phone number.

You can test the API in a command line interface as shown in the example below

curl --request POST --url https://api-sandbox.pymnt.us/shortcode/ \
--header 'Authorization: ACCESS_TOKEN' \
--header 'x-api-key: MY_APPLICATION_KEY' \
--data '{"AMOUNT":"121.81","PHONE":"9180818001","INVOICE":"180818101"}'


The API call will return a short code payment link as shown in the example below

{"CREATED_DATE":"2018-08-27T18:28:24.849Z","URL":"https://pymnt.us/pd/5af39","SHORT_CODE":"5af39"}

Next, let’s test our first payment.

Getting started

Testing your first payment

Configure an email address where the business will receive payment confirmation

Webhook notification of payment status

A webhook is a request that RPN sends to your server to alert you of an event. Adding support for webhooks allows you to receive real-time notifications from RPN when things happen in your account, so you can take automated actions in response, for example:

  • When a payment fails due to card denied or insufficient funds
  • When a customer’s payment succeeds, record that payment against the associated invoice

To start receiving webhooks, you’ll need to provide a webhook endpoint. Webhooks are HTTP POST requests made to the URL you provided, with a JSON body. The first step to take when you receive a webhook is to check its signature - this makes sure that is genuinely from RPN and hasn’t been forged. A signature is provided in the Webhook-Signature header of the request. We just need to compute the signature ourselves using the POSTed JSON and the webhook endpoint’s secret, and compare it to the one in the header. If they match, the webhook is genuine, because only you and RPN know the secret. It’s important that you keep the secret safe, and change it periodically using the Dashboard.

RPN notifications to your the webhook endpoint contains the following payload.

{
MID: MERCHANT ID
INVOICE:Invoice Number
AMOUNT:Invoice Payment Amount
STATUS:SUCCESS/FAIL
PAYMNET_ID:Transaction ID from the payment gateway
}


When you sign up in the sandbox, we’ll give you access to collect USD payments in the United States. If you want to use other locales and currencies, just get in touch.

Shortcode Generation

Generate the payment short code for your invoice

Send Payment Link

Text message the payment link to your mobile phone number

Complete Payment

Make a payment using your mobile wallet or card information

Congratulations!

Once you receive a confirmation of the payment, your integration is complete.



Getting started

Staying up-to-date with webhooks

A webhook is a request that RPN sends to your server to alert you of an event. Adding support for webhooks allows you to receive real-time notifications from RPN when things happen in your account, so you can take automated actions in response, for example:

  • When a payment fails due to lack of funds, retry it automatically
  • When a customer cancels their mandate with the bank, suspend their account
  • When a customer’s subscription generates a new payment, record that payment against their account

Making localhost accessible to the internet with ngrok

To start experimenting with webhooks, your code will need to be accessible from the internet so RPN can reach it with HTTP requests. If you’re working locally, the easiest way to do this is with ngrok.

Download it and follow the instructions on the page to install it (you’ll need to unzip the archive, and then make the ngrok binary available in your PATH).

Now, just run ngrok http , where is the port where your web server is running (for example ngrok http 8000 if you’re running on port 8000).

You’ll see two links that forward to localhost:8000. Select the URL that starts with https, and copy it to your clipboard.

In the sandbox, you can use an HTTP URL for your webhooks, but to go live, the endpoint must be HTTPS secured.

Adding a webhook URL in the RPN Dashboard

To start receiving webhooks, you’ll need to add a webhook endpoint from your Dashboard here.

Simply enter the HTTPS URL from earlier and add on the path where your webhook handler will be available, give your endpoint a name, and then click “Create webhook endpoint”. Next, click on your new endpoint in the list and copy the secret to your clipboard.

Building a webhook handler

Let’s get started by building our first webhook handler. Webhooks are HTTP POST requests made to the URL you provided, with a JSON body.

The first step to take when you receive a webhook is to check its signature - this makes sure that is genuinely from RPN and hasn’t been forged. A signature is provided in the Webhook-Signature header of the request. We just need to compute the signature ourselves using the POSTed JSON and the webhook endpoint’s secret (which we copied earlier), and compare it to the one in the header.

If they match, the webhook is genuine, because only you and RPN know the secret. It’s important that you keep the secret safe, and change it periodically using the Dashboard.

We can verify the signature like this:

<?php
// We recommend storing your webhook endpoint secret in an environment variable
// for security, but you could include it as a string directly in your code
$webhook_endpoint_secret = getenv("RPN_WEBHOOK_ENDPOINT_SECRET");
$request_body = file_get_contents('php://input');

$headers = getallheaders();
$signature_header = $headers["Webhook-Signature"];

try {
    $events = \RPNPro\Webhook::parse($request_body,
                                            $signature_header,
                                            $webhook_endpoint_secret);

    // Process the events...

    header("HTTP/1.1 204 No Content");
} catch(\RPNPro\Core\Exception\InvalidSignatureException $e) {
    header("HTTP/1.1 498 Invalid Token");
}
# Here, we're using a Django view, but essentially the same code will work in
# other Python web frameworks (e.g. Flask) with minimal changes
import json
import hmac
import hashlib
import os

from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import HttpResponse

# Handle the incoming Webhook and check its signature.
class Webhook(View):
    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super(Webhook, self).dispatch(*args, **kwargs)

    def is_valid_signature(self, request):
        secret = bytes(os.environ['GC_WEBHOOK_SECRET'], 'utf-8')
        computed_signature = hmac.new(
            secret, request.body, hashlib.sha256).hexdigest()
        provided_signature = request.META["HTTP_WEBHOOK_SIGNATURE"]
        # In flask, access the webhook signature header with
        # request.headers.get('Webhook-Signature')
        return hmac.compare_digest(provided_signature, computed_signature)

    def post(self, request, *args, **kwargs):
        if self.is_valid_signature(request):
            return HttpResponse(200)
        else:
            return HttpResponse(498)
# Here, we're using a Rails controller, but essentially the same code will work in other
# Ruby web frameworks (e.g. Sinatra) with minimal changes

class WebhooksController < ApplicationController
  include ActionController::Live

  protect_from_forgery except: :create

  def create
    # We recommend storing your webhook endpoint secret in an environment variable
    # for security, but you could include it as a string directly in your code
    webhook_endpoint_secret = ENV['RPN_WEBHOOK_ENDPOINT_SECRET']

    # In a Rack app (e.g. Sinatra), access the POST body with
    # `request.body.tap(&:rewind).read`
    request_body = request.raw_post

    # In a Rack app (e.g. Sinatra), the header is available as
    # `request.env['HTTP_WEBHOOK_SIGNATURE']`
    signature_header = request.headers['Webhook-Signature']

    begin
      events = RPNPro::Webhook.parse(request_body: request_body,
                                            signature_header: signature_header,
                                            webhook_endpoint_secret: webhook_endpoint_secret)

      # Process the events...

      render status: 204, nothing: true
    rescue RPNPro::Webhook::InvalidSignatureError
      render status: 498, nothing: true
    end
  end
end
package hello;

// Use the POM file at
// https://raw.githubusercontent.com/RPN/RPN-pro-java-maven-example/master/pom.xml

import com.RPN.errors.InvalidSignatureException;
import com.RPN.resources.Event;
import com.RPN.Webhook;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;

@RestController
public class HelloController {
    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signatureHeader,
            @RequestBody String requestBody) {
        String webhookEndpointSecret = System.getenv("RPN_WEBHOOK_ENDPOINT_SECRET");

        try {
            List<Event> events = Webhook.parse(requestBody, signatureHeader, webhookEndpointSecret)

            for (Event event : events) {
                // Process the event
            }

            return new ResponseEntity<String>("OK", HttpStatus.OK);
        } catch(InvalidSignatureException e) {
            return new ResponseEntity<String>("Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}
[HttpPost]
public ActionResult HandleWebhook()
{
    var requestBody = Request.InputStream;
    requestBody.Seek(0, System.IO.SeekOrigin.Begin);
    var requestJson = new StreamReader(requestBody).ReadToEnd();

    // We recommend storing your webhook endpoint secret in an environment variable
    // for security, but you could include it as a string directly in your code
    var secret = ConfigurationManager.AppSettings["RPNWebhookSecret"];

    var hmac256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    byte[] messageHash = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestJson));
    var result = BitConverter.ToString(messageHash).Replace("-", "").ToLower();

    // If the signature doesn't match what was expected, reject the request
    if ((Request.Headers["Webhook-Signature"] ?? "") != result)
      return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

    // Otherwise, handle the events in the webhook and respond with 200 OK
    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

Testing your webhook handler

In the Dashboard, the “Send test web hook” functionality makes it easy to start experimenting with webhooks.

Select the webhook endpoint you created earlier, set the Resource type of mandates and the action to cancelled. You can set the cause and event details however you like. Then, click “Send test web hook”.

Now, refresh the page. Usually, the webhook will appear right away, but sometimes you might have to refresh a few times. We’ll make a request to your endpoint, and your webhook will appear in the list - click on it. If everything’s working, you’ll see a response code of 200 OK.

Processing events

A webhook can contain multiple events, and each has a resource_type (telling us what kind of resource the event is for, for example “payments” or “mandates”), an action (telling us what happened to the resource, for example the cancelled action for a mandate) and details (specifying why the event happened).

You can see a full list of the possible combinations in the reference docs.

As an example, we’ll write a handler for when a mandate is cancelled:

<?php
function process_mandate_event($event)
{
    switch ($event->action) {
        case "cancelled":
            print("Mandate " . $event->links["mandate"] . " has been cancelled!\n");

            // You should keep some kind of record of what events have been processed
            // to avoid double-processing
            //
            // $event = Event::where("RPN_id", $event->id)->first();

            // You should perform processing steps asynchronously to avoid timing out if
            // you receive many events at once. To do this, you could use a queueing
            // system like Beanstalkd
            //
            // http://george.webb.uno/posts/sending-php-email-with-mandrill-and-beanstalkd
            //
            // Once you've performed the actions you want to perform for an event, you
            // should make a record to avoid accidentally processing the same one twice.
            CancelServiceAndNotifyCustomer::performAsynchronously($event->links["mandate"]);
            break;
        default:
            print("Don't know how to process a mandate " . $event->action . " event\n");
            break;
    }
}

// We recommend storing your webhook endpoint secret in an environment variable
// for security, but you could include it as a string directly in your code
$webhook_endpoint_secret = getenv("RPN_WEBHOOK_ENDPOINT_SECRET");
$request_body = file_get_contents('php://input');

$headers = getallheaders();
$signature_header = $headers["Webhook-Signature"];

try {
    $events = \RPNPro\Webhook::parse($request_body,
                                            $signature_header,
                                            $webhook_endpoint_secret);

    foreach ($events as $event) {
        print("Processing event " . $event->id . "\n");

        switch ($event->resource_type) {
            case "mandates":
                process_mandate_event($event);
                break;
            default:
                print("Don't know how to process an event with resource_type " . $event->resource_type . "\n");
                break;
        }
    }

    header("HTTP/1.1 200 OK");
} catch(\RPNPro\Core\Exception\InvalidSignatureException $e) {
    header("HTTP/1.1 498 Invalid Token");
}
# Here, we're using a Django view, but essentially the same code will work in
# other Python web frameworks (e.g. Flask) with minimal changes

import json
import hmac
import hashlib
import os

from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import HttpResponse

# Handle the incoming Webhook and perform an action with the  Webhook data.
class Webhook(View):
    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super(Webhook, self).dispatch(*args, **kwargs)

    def is_valid_signature(self, request):
        secret = bytes(os.environ['GC_WEBHOOK_SECRET'], 'utf-8')
        computed_signature = hmac.new(
            secret, request.body, hashlib.sha256).hexdigest()
        provided_signature = request.META["HTTP_WEBHOOK_SIGNATURE"]
        return hmac.compare_digest(provided_signature, computed_signature)

    def post(self, request, *args, **kwargs):
        if self.is_valid_signature(request):
            response = HttpResponse()
            payload = json.loads(request.body.decode('utf-8'))
            # Each webhook may contain multiple events to handle, batched together.
            for event in payload['events']:
                self.process(event, response)
            return response
        else:
            return HttpResponse(498)

    def process(self, event, response):
        response.write("Processing event {}\n".format(event['id']))
        if event['resource_type'] == 'mandates':
            return self.process_mandates(event, response)
        # ... Handle other resource types
        else:
            response.write("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))
            return response

    def process_mandates(self, event, response):
        if event['action'] == 'cancelled':
            response.write("Mandate {} has been \
                cancelled\n".format(event['links']['mandate']))
        # ... Handle other mandate actions
        else:
            response.write("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))
        return response
require 'RPN_pro'

class MandateEventProcessor
  def self.process(event, response)
    case event.action
    when 'cancelled'
      response.stream.write("Mandate #{event.links.mandate} has been cancelled\n")

      # You should keep some kind of record of what events have been processed
      # to avoid double-processing, checking if the event already exists before
      # event = Event.find_by(RPN_id: event['id'])

      # You should perform processing steps asynchronously to avoid timing out
      # if you receive many events at once. To do this, you could use a
      # queueing system like
      # Resque (https://github.com/resque/resque)
      #
      # Once you've performed the actions you want to perform for an event, you
      # should make a record to avoid accidentally processing the same one twice
      # CancelServiceAndNotifyCustomer.enqueue(event['links']['mandate'])
    else
      response.stream.write("Don't know how to process a mandate #{event.action} " \
                            "event\n")
    end
  end
end

def create
  # We recommend storing your webhook endpoint secret in an environment variable
  # for security, but you could include it as a string directly in your code
  webhook_endpoint_secret = ENV['RPN_WEBHOOK_ENDPOINT_SECRET']

  # In a Rack app (e.g. Sinatra), access the POST body with
  # `request.body.tap(&:rewind).read`
  request_body = request.raw_post

  # In a Rack app (e.g. Sinatra), the header is available as
  # `request.env['HTTP_WEBHOOK_SIGNATURE']`
  signature_header = request.headers['Webhook-Signature']

  begin
    events = RPNPro::Webhook.parse(request_body: request_body,
                                          signature_header: signature_header,
                                          webhook_endpoint_secret: webhook_endpoint_secret)

    events.each do |event|
      # We're using Rails's streaming functionality here to write directly to the
      # response rather than using views, as one would usually.
      response.stream.write("Processing event #{event.id}\n")

      case event.resource_type
      when 'mandates'
        MandateEventProcessor.process(event, response)
      else
        response.stream.write("Don't know how to process an event with resource_type " \
                              "#{event.resource_type}\n")
      end
    end

    response.stream.close
    render status: 200
  rescue RPNPro::Webhook::InvalidSignatureError
    render status: 498
  end
end
package hello;

// Use the POM file at
// https://raw.githubusercontent.com/RPN/RPN-pro-java-maven-example/master/pom.xml

import com.RPN.errors.InvalidSignatureException;
import com.RPN.resources.Event;
import com.RPN.Webhook;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;

@RestController
public class HelloController {
    private String processMandate(Event event) {
    /*
      You should keep some kind of record of what events have been processed
      to avoid double-processing, checking if the ecent already exists
      before processing it.

      You should perform processing steps asynchronously to avoid timing out
      if you receive many events at once. To do this, you could use a
      queueing system like @Async.

      https://spring.io/guides/gs/async-method/

      Once you've performed the actions you want to perform for an event, you
      should make a record to avoid accidentally processing the same one twice
    */
        switch (event.getAction()) {
            case "cancelled":
                return "Mandate " + event.getLinks().getMandate() +
                    " has been cancelled.\n";
            default:
                return "Do not know how to process an event with action " +
                    event.getAction() + ".\n";
        }
    }

    private String processEvent(Event event) {
        switch (event.getResourceType()) {
            case MANDATES:
                return processMandate(event);
            default:
                return "Don't know how to process an event of resource_type " +
                    event.getResourceType().toString() + ".\n";
        }
    }

    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signatureHeader,
            @RequestBody String requestBody) {
        String webhookEndpointSecret = System.getenv("RPN_WEBHOOK_ENDPOINT_SECRET");

        try {
            List<Event> events = Webhook.parse(requestBody, signatureHeader, webhookEndpointSecret)

            String responseBody = "";

            for (Event event : events) {
                responseBody += processEvent(event);
            }

            return new ResponseEntity<String>(responseBody, HttpStatus.OK);
        } catch(InvalidSignatureException e) {
            return new ResponseEntity<String>("Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}
[HttpPost]
public ActionResult HandleWebhook()
{
    var requestBody = Request.InputStream;
    requestBody.Seek(0, System.IO.SeekOrigin.Begin);
    var requestJson = new StreamReader(requestBody).ReadToEnd();

    // We recommend storing your webhook endpoint secret in an environment variable
    // for security, but you could include it as a string directly in your code
    var secret = ConfigurationManager.AppSettings["RPNWebhookSecret"];

    var hmac256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    byte[] messageHash = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestJson));
    var result = BitConverter.ToString(messageHash).Replace("-", "").ToLower();

    // If the signature doesn't match what was expected, reject the request
    // Otherwise, handle the events in the webhook then respond with 200 OK
    if ((Request.Headers["Webhook-Signature"] ?? "") != result)
      return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

    // Here we deserialize the webhook using the same objects the library uses to
    // represent API responses for Events, but you could parse and handle the JSON
    // body however you prefer.
    var serializerSettings = new RPN.Internals.JsonSerializerSettings();
    RPN.Services.EventListResponse events =
        JsonConvert.DeserializeObject<RPN.Services.EventListResponse>(message, serializerSettings);

    // Webhooks can contain many events. In a real implementation, you should handle
    // the processing of each event asychronously to avoid timing out here.
    // You could check whether you've processed the event before (by recording the event
    // ID when you process it) and retrieve the associated resource to ensure that you have
    // the most up-to-date information about it.
    foreach (RPN.Resources.Event eventResource in events.Events)
    {
        // To keep this example simple, we're only handling Mandate events
        if (eventResource.ResourceType == RPN.Resources.EventResourceType.Mandates)
        {
            switch (eventResource.Action)
            {
                case "created":
                    Console.WriteLine($"Mandate {eventResource.Links.Mandate} has been created, yay!");
                    break;
                case "cancelled":
                    Console.WriteLine($"Oh no, mandate {eventResource.Links.Mandate} was cancelled!");
                    break;
                default:
                    Console.WriteLine($"{eventResource.Links.Mandate} has been {eventResource.Action}");
                    break;
            }
        }
        else
        {
            Console.WriteLine($"No method to handle {eventResource.ResourceType} events yet");
        }
    }
    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

Send a test webhook again, just like the one you sent before, and then click on it in the list. You’ll see a response at the bottom like this:

Processing event EVTEST8XP9DCPK
Mandate index_ID_123 has been cancelled!

We’ll send webhooks exactly like this when real events happen in your account.

In the sandbox, to make integrating as easy as possible, we provide scenario simulators which allow you to manually trigger certain cases (like a customer cancelling their mandate or a payment failing due to insufficient funds) from the Dashboard so you can test how your integration responds.

You can add support for as many differ resource_types and actions as you like, and make use of all of the other data we give you with events.

Did you find this page helpful? Yes No

Getting started

What's next?

Once you’ve fully tested your integration in the sandbox, you’ll need to have it reviewed by a member of the RPN team before you can start connecting merchants in the live environment, where your business customers can create a payment link tied to every invoice generated, send it to their customers, collect the invoiced payments and receive payment notifications. You now have the building blocks of a powerful payment integration that allows businesses to use your application to bill and seamlessly collect payments from their customers.

Going live

To go live - simply sign up for your live account. Once your live account is set up, create an access token and then switch the environment and the access token in your code.

Your sandbox account will stay active, and is perfect for testing any tweaks you want to make to your integration.

Your next steps

Getting started

Handling customer notifications

RPN automatically sends notifications to customers whenever an event happens in our system. For example, when a payment has been created, the customer will receive an email which informs them about the amount, description and cause of the payment.

Integrators have the ability to send some of these notifications themselves, and we’ll honour their request if it is made within our stated deadline: otherwise, we will send the notification ourselves.

Here is an example of the workflow:

  • Your integration creates a payment
  • The payment event schedules a payment_created notification
  • We send a webhook to your application which includes notification metadata
  • Your application tells us whether it would like to handle the notification

There are two conditions that you must satisfy to be able to handle or ignore a notification:

  • You must be approved to handle notifications of this type
  • You must be the “notification owner” for that notification/resource
Getting approved to handle notifications

To be able to handle customer notifications yourself, you will need to be granted permission. Please get in touch with the RPN onboarding team to get started. Permissions are very granular, and cover the type & scheme of the notification. For example, you might be approved to handle the payment_created notification just in the scheme sepa.

Compliant notifications must include all of the required information for that notification type, and be sent within the correct interval. More information about the required information for each type of notification can be found in our platform guides.

Becoming a notification owner

A merchant may be connected to multiple partners, or have multiple integrations, so to avoid the problem of multiple integrations all trying to handle the same notification, we track the “owner” for all customer notifications in our system.

Only the owner will have the ability to handle notifications for that resource, falling back to RPN if the notification deadline is missed.

The owner is usually defined as the creator of that resource. For example, if your integration creates a payment, the owner will be your integration. Currently our system does not permit any kind of ownership transfer from one integration to another. Resources created via dashboard will not record any owner (and therefore cannot be handled by your integration).

Getting notified about notifications

Notification information is currently only delivered in webhooks. When your application receives a webhook, each event may include a customer_notifications payload, which contains a list of one or more notifications that were triggered by that event. (If you don’t receive this information, it’s because you are not the owner for that resource or have not been granted any permissions):

POST https://example.com/webhooks HTTP/1.1
Content-Type: application/json
{
  "events": [
    {
      "id": "EV123",
      "created_at": "2018-08-03T12:00:00.000Z",
      "action": "created",
      "resource_type": "payments",
      "customer_notifications": [
        {
          "id": "PCN123",
          "type": "payment_created",
          "deadline": "2018-08-25T12:09:06.000Z",
          "mandatory": true
        }
      ]
    }
  ]
}

Note that each notification for an event includes an id, type, deadline and mandatory flag:

  • id is used to handle the notification (see below)
  • type can be used to filter notifications that you don’t handle. For example, if your integration only handles payment_created notifications, you can safely ignore notifications which are of another type.
  • deadline is the time by which your application must respond. If we don’t hear from you before this deadline, we’ll send the notification ourselves. The deadline is typically 10 minutes from the point at which we send the webhook, but in the case of payment_created notifications the deadline is the last possible time to notify the customer before the payment collects (this mirrors RPN’ behaviour).
  • mandatory is whether the notification needs to be handled by somebody (currently always true).
Handling notifications yourself

If your application intends to handle a notification, you should let us know before you take action. If you wait until after sending the notification, there is a chance that we also sent it in the meantime (e.g. if the deadline had elapsed), which would result in the customer receiving two notifications, one from each of us.

So, we recommend that you declare your intent first, and then send the notification using whichever mechanism is most appropriate for your system:

require "RPN_pro"

client = RPNPro::Client.new(
  access_token: ENV["GC_ACCESS_TOKEN"],
  environment: :sandbox,
)

RPNPro::Webhook.parse(**args).each do |event|
  notifications = event.customer_notifications

  # We might want to handle mandate_created emails and send an email which includes
  # both the mandate and upcoming payments for it.
  mandate_notifications = notifications.select { |n| n.type == "mandate_created" }

  mandate_notifications.each do |notification|
    client.customer_notifications.handle(notification.id)
    MyNotificationSystem.enqueue_email(notification, event)
  end

  # If a payment_created email was already sent, we still need to handle the notification
  # to prevent RPN from sending it.
  payment_notifications = notifications.select { |n| n.type == "payment_created" }

  payment_notifications.each do |notification|
    if notification_sent_previously?(notification)
      client.customer_notifications.handle(notification.id)
    end
  end
end

Alternatively, if your application does not respond, or does not respond before the deadline, RPN will send the notification instead and there will be no opportunity to handle after that point.

In all cases, RPN will record the outcome for each notification for audit purposes.

Getting started

Introduction

The RPN API is a wrapper around national Direct Debit systems in the UK, Eurozone, Sweden, Denmark and Australia, allowing you to collect payments anywhere with one integration, pulling payments direct from customers’ bank accounts.

With the RPN API, it’s easy to build a partner integration, adding Direct Debit functionality to your software, so your users can collect and manage their payments in a way that’s seamlessly integrated with your product and their business processes.

For example, if you provide accounting and invoicing software, you can integrate Direct Debit using the RPN API so your users can get paid automatically every time they raise an invoice. But it’s not just finance software that works great with RPN - we’ve seen partner integrations built with all kinds of software from tools for running gyms to WordPress blogs.

If you want to collect Direct Debit for yourself, rather than allowing your users to collect payments through your product, head over to our API integration guide instead.

Integrating with RPN has the power to add huge value for your users, streamlining their businesses processes and providing a great experience.

As well as delighting your users, you may be entitled to a share of our revenue for each payment collected through your integration, or you can add your own fees on top of RPN’s. If you’re interested in this, please contact our partnerships team before you start your integration.

In this guide, we’ll cover all of the key steps of building a great integration, including:

  • Connecting your users’ existing or newly created RPN accounts to your product via OAuth
  • Allowing your users to set up Direct Debit mandates with their customers
  • Initiating payments against those mandates on behalf of your users
  • Handling our webhooks, so you can stay up-to-date on what happens to your users’ mandates and payments

Every partner integration is different, so this guide can’t tell you exactly what to do, but we’ll explain the flows you should handle, and give you some ideas of how to provide the best experience for your users.

If you haven't spoken to us yet about your partner integration, please contact our partnerships team before getting started.

Did you find this page helpful? Yes No

Getting started

Connecting your users' accounts

In this first step of the partner integration guide, we’ll take you through securely gaining access to your users’ RPN accounts.

You’ll end up with an access token, which we’ll be able to use later on in this guide to set up mandates and payments on your user’s behalf.

OAuth: an introduction

To set up mandates and payments for your users, you’ll need access to their RPN account so you can make API requests on their behalf.

Our OAuth flow allows you to securely obtain an access token for a RPN account without having to have your user’s password or do any manual setup.

There are four key benefits to getting access to your users’ accounts using OAuth:

  • It’s secure: Your users have a secure way to grant you access to their account without needing to send you their login credentials or an access token. They only need to tell us that you may have access through a simple online process.
  • It’s fast : You only need to generate and send your user to a link to our OAuth flow. From there, they’ll create a RPN account or sign in to their existing one, approve your access to their account, and then we’ll send them right back to you with an access token.
  • It’s lucrative: Linking to your users’ accounts using OAuth makes you eligible to earn commission for each payment they process through your product, either by adding your own fees on top of RPN’s, or by receiving a share of RPN’s fees
  • It’s future-proof: As the RPN API gets better and better over time and offers new functionality, you’ll be able to stay up-to-date and use all the latest features

The process for your user looks like this:

  • Your user clicks a button in your UI to start the process of connecting with RPN
  • You generate a link to the RPN OAuth flow, embedding details of your app (which we’ll set up next) as well as a scope to specify the level of access you want your user to give you
  • You redirect your user to the generated link. Your user lands on our site, where they create a RPN account or log in to their existing one, and agree to grant your app access
  • We redirect your user’s browser back to you with a code query parameter
  • You exchange the code for a permanent access token, which you store so that you can use it to make API requests on your user’s behalf

Using the OAuth flow

Creating an app

Before we can get started, you need to create an “app” in the sandbox environment which represents your partner integration in the RPN system. You’ll be issued with a client ID and secret, which you’ll use to identify yourself when sending users to the OAuth flow and swapping codes for access tokens.

First sign up for a RPN account in our sandbox testing environment and then create an app.

You’ll need to provide a name, a description and the URL of your homepage (where users can go to read more about your product or your integration with RPN). We won’t specify the post-onboarding URL or webhook URL for now.

Once your user has completed the OAuth flow, they will be redirected to your application using the redirect_url parameter you provide. For security reasons, this can’t be just any internet URL, so you will need to configure at least one redirect URL here (which must exactly match the parameter). You may wish to add additional redirect URLs, for example when also testing against your local application, but we’ll only allow up to 20 redirect URLs in total.

Once you’ve created an app, you’ll see it in the list of your apps in your Dashboard. Click on your newly-created app, and take a note of the Client ID and Client Secret.

Generating a link to the OAuth flow

Let’s get started by installing an OAuth client library:

composer require adoy/oauth2
pip install oauth2client
echo "gem 'oauth2'" >> Gemfile
bundle install
<-- Add the following to your Maven file --> 
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.4.1</version>
</dependency>

Now we’ve installed the library, we can build a link to the OAuth flow. We’ll have to pass in a number of parameters, including the client_id and client_secret, as well as a few parameters that will vary by library. These can include:

The URL of the RPN OAuth flow
In the sandbox, this is https://connect-sandbox.pymnt.us/oauth/authorize. It'll form the base of the URL your client library generates.
A redirect URL
The URL to send your users to once they've agreed to connect their account to RPN (as well if they deny authorisation or something goes wrong, in which case we'll pass you details of the error). This must exactly match one of the redirect URLs you specified above.
The URL of the RPN access token API
The URL of the API endpoint which will be used to exchange the code for an access token after your user has been redirected back to your redirect URL. In the sandbox environment, this is https://connect-sandbox.pymnt.us/oauth/access_token.
scope
The level of access you want to your users' RPN accounts - this may either be read_write or read_only.
initial_view (optional)
An optional parameter, set to either signup or login to specify which view we should show when your user enters the OAuth flow. By default, they will see the login view if you're requesting read_only access and the signup view if requesting read_write.
state (optional)
Any value you pass in here will be included as a querystring parameter when we redirect back to your redirect URL. Please note that this value can be tampered with by your user, and so shouldn't be trusted implicitly. We recommend using this parameter for a CSRF token.
prefill[email] (optional)
Your user's email address. We will pre-fill this on the login or signup forms to make it quicker and easier for them to complete the flow.
prefill[given_name] (optional)
Your user's given (first) name. We will pre-fill this on the signup form.
prefill[family_name] (optional)
Your user's family (last) name. We will pre-fill this on the signup form.
prefill[organisation_name] (optional)
The name of the user's organisation (e.g. Acme Widget plc, 2nd Upminster Scout Group or Tim Rogers). We will pre-fill this on the signup form.
language (optional)
The language that the login/signup form should be in, in ISO 639-1 format. If the language specified is supported, we will use it. Otherwise, we will fall back to the most appropriate available language for the user, based on factors like their browser settings and location.

With these parameters set correctly, the resulting URL will have the following format:

https://connect-sandbox.pymnt.us/oauth/authorize?client_id=myid&initial_view=signup&prefill%5Bemail%5D=tim%40pymnt.us&redirect_uri=https%3A%2F%2Facme.enterprises%2Fredirect&response_type=code&scope=read_only

Run this code to generate a link to the OAuth flow. Head over to the link you’ve just generated in an Incognito window, and you’ll see our OAuth flow.

Getting an access token

On the signup form, we recommend creating a second sandbox account using a different email address. This will replicate what your users will experience when connecting to your partner integration, and will give you an example account which you can use from the perspective of one of your users.

Once your user has either signed up or logged in, and then approved your app’s access to their account, they’ll be sent to your app’s redirect URI with a temporary code which you’ll see in the query parameters, as well as any state you provided.

You should use the OAuth client library we set up earlier to fetch an access token using the code. This is a permanent access token which allows you to use the API on behalf of your merchant at any time.

<?php
require 'vendor/autoload.php';

$client = new OAuth2\Client(getenv('RPN_CLIENT_ID'),
                            getenv('RPN_CLIENT_SECRET'));

// You'll need to use exactly the same redirect URI as in the last step
$response = $client->getAccessToken(
    'https://connect-sandbox.pymnt.us/oauth/access_token',
    'authorization_code',
    ['code' => $_GET['code'], 'redirect_uri' => 'https://acme.enterprises/redirect']
);

$payload = ['RPN_access_token' => $response['result']['access_token'],
            'RPN_organisation_id' => $response['result']['organisation_id']];

$currentUser->update($payload);
import os
from oauth2client.client import OAuth2WebServerFlow

# You should store your client ID and secret in environment variables rather than
# committing them with your code
flow = OAuth2WebServerFlow(
        client_id=os.environ['RPN_CLIENT_ID'],
        client_secret=os.environ['RPN_CLIENT_SECRET'],
        scope="read_write",
        # You'll need to use exactly the same redirect URI as in the last step
        redirect_uri="https://acme.enterprises/redirect",
        # Once you go live, this should be set to https://connect.pymnt.us. You'll
        # also need to create a live app and update your client ID and secret.
        auth_uri="https://connect-sandbox.pymnt.us/oauth/authorize",
        token_uri="https://connect-sandbox.pymnt.us/oauth/access_token",
        initial_view="signup"
)

access_token = flow.step2_exchange(request.args.get('code'))

# You should store the user's access token - you'll need it to make API requests on their
# behalf in future. If you want to handle webhooks for your users, you should also store
# their organisation ID which will allow you to identify webhooks belonging to them.
current_user.update(
    RPN_access_token=access_token.access_token,
    RPN_organisation_id=access_token.token_response['organisation_id']
)
require 'oauth2'

# You should store your client ID and secret in environment variables rather than
# committing them with your code
oauth = OAuth2::Client.new(ENV['RPN_CLIENT_ID'],
                           ENV['RPN_CLIENT_SECRET'],
                           # Once you go live, this should be set to
                           # https://connect.pymnt.us. You'll also need to create
                           # a live app and update your client ID and secret.
                           site: 'https://connect-sandbox.pymnt.us',
                           authorize_url: '/oauth/authorize',
                           token_url: '/oauth/access_token')

access_token = oauth.auth_code.get_token(
  params[:code],
  # You'll need to use exactly the same access token as you did in the last step
  redirect_uri: 'https://acme.enterprises/redirect'
)

# You should store the user's access token - you'll need it to make API requests on their
# behalf in future. If you want to handle webhooks for your users, you should also store
# their organisation ID which will allow you to identify webhooks belonging to them.
current_user.update!(RPN_access_token: access_token.token,
                     RPN_organisation_id: access_token['organisation_id'])
// See https://raw.githubusercontent.com/RPN/RPN-pro-java-maven-example/master/src/main/java/hello/OAuthController.java

import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
public class OAuthController {
  private static final String accessTokenUrl =
          "https://connect-sandbox.pymnt.us/oauth/access_token";
  private static final String redirectUri =
          "https://bb314345.ngrok.io/redirect";
  private static final String clientID =
          System.getenv("RPN_CLIENT_ID");
  private static final String clientSecret =
          System.getenv("RPN_CLIENT_SECRET");

  private static final OkHttpClient client =
          new OkHttpClient();

  private String getAccessToken(String oauthCode) throws IOException {
      RequestBody body = new FormBody.Builder()
        .add("client_id", clientID)
        .add("client_secret", clientSecret)
        .add("redirect_uri", redirectUri)
        .add("grant_type", "authorization_code")
        .add("code", oauthCode)
        .build();

      Request request = new Request.Builder()
        .url(accessTokenUrl)
        .post(body)
        .build();
      try (Response response = client.newCall(request).execute()) {
          return response.body().string();
      }
  }

  @GetMapping("/redirect")
  public String loggedIn(@RequestParam("code") String oauthCode) throws IOException {
      String accessToken = getAccessToken(oauthCode);
      System.out.println("Save the access token " +
              accessToken + " to your database.");
      return "Greetings!";
  }
}

Whether you are developing a mobile, web, or desktop application, it is important not to pass the client secret to your user’s device as it could be used to impersonate your app. The process of exchanging a code for an access token should be done on your server so your client secret can be kept private.

You’ll want to store this access token in your database for use in the future to make requests to the RPN API on your user’s behalf. Make sure you keep it safe and secure, as it gives full access to your user’s account.

Once your user has connected to your app, they can revoke your access from the RPN dashboard, or you can revoke (i.e. invalidate) their access token yourself using the API.

We’d also strongly advise storing your user’s RPN organisation ID. Later on, we’ll set up webhooks to keep your integration up to date as things happen to your users’ mandates and payments. You’ll need to use the organisation ID included in these webhooks to work out which of your users a particular event relates to.

If you didn't store the user's RPN organisation ID and want it later, you can find it out by looking up the access token using the API.

If the redirect URL isn’t the page where you ultimately want your user to arrive, you can redirect them from here to the right place, either before or after exchanging the code for an access token.

Once you’ve stored your merchant’s access token, we’d suggest presenting your customers with any relevant configuration options for the integration (e.g. setting how failed payments should be handled).

We’d also recommend providing in-product tips or links to support materials and letting customers know that they’ll need to verify their account in order to receive payouts (they’ll also receive emails from us reminding them to do this).

To get the best out of your integration, we’d suggest adding it to your onboarding experience and prompting merchants to set up with RPN at key points in their product journey (e.g. setting up an invoice, creating a customer, signing a contract etc.). From this process, you'd then send them into the OAuth flow. Take a look at some of our recommendations for bringing your integration to your users' attention here.

Did you find this page helpful? Yes No

Getting started

Helping your users get verified

Before your users can receive payouts of the payments they’ve collected, they’ll need toverify their account. As part of the verification process, your users will need to:

  • Provide information about their business
  • Upload proof of identity and address
  • Add their bank account details, and prove they own the account

We collect these details through our dedicated onboarding flow in order to comply withanti-money laundering regulations. Once a user has signed up to RPN through your app, you should send them straight to the onboarding flow to provide these details.

In this guide, we’ll show you how to set up the RPN client library so you can access our API using your users’ access tokens.

Once that’s set up, we’ll show you how to query your users’ verification status and, if required, how to send them to our onboarding flow. We’ll collect the information we need and then send the user back to your application.

Setting up your client library

To make it quick and easy to interact with the RPN API, we maintain libraries inJava,Python,Ruby, PHP, and .NET to talk to our API in those languages without writing lots of boilerplate code.

Of course, you can still use our API if we don’t have a library in your language by building the HTTP requests yourself - see our API reference to get started.

Let’s install the API library using a package manager (you can also, if you prefer, download the source yourself from the links above):

# We strongly recommend using Composer, but if you'd prefer to
# install the PHP library manually, see the instructions at
# https://github.com/RPN/RPN-pro-php#manual-installation
composer require RPN/RPN-pro "~1.1"
pip install RPN_pro
echo "gem 'RPN_pro'" >> Gemfile
bundle install
# Add the following to <dependencies> in your pom.xml
<dependency>
    <groupId>com.RPN</groupId>
    <artifactId>RPN-pro</artifactId>
    <version>3.0.0</version>
</dependency>
# Install our Nuget package:
install-package RPN

Our API libraries have pro in their names, but don’t worry: if you’re starting a new integration, these are the right libraries for you.

Now we can import the library into our code so that it’s ready to use:

require 'vendor/autoload.php';
import RPN_pro
# In a Rails app, this will happen automatically
require 'RPN_pro'
import com.RPN.RPNClient;
using RPN;

You’re now ready to initialise the client - wherever you’re making an API call on behalf of one of your users, you’ll need to pull their RPN access token from your database and instantiate the client object with it:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    // You'll need to identify the user the current user and fetch their access token
    'access_token' => $currentUser->RPNAccessToken,
    // Change me to LIVE when you're ready to go live
    'environment' => \RPNPro\Environment::SANDBOX
]);
import RPN_pro

from myintegration import current_user

client = RPN_pro.Client(
    # You'll need to identify the user the current user and fetch their access token
    access_token=current_user.RPN_access_token,
    # Change this to 'live' when you are ready to go live.
    environment='sandbox'
)
require 'RPN_pro'

client = RPNPro::Client.new(
  # You'll need to identify the user the current user and fetch their access token
  access_token: current_user.RPN_access_token,
  # Remove the following line when you're ready to go live
  environment: :sandbox
)
package com.gcintegration;

import com.RPN.RPNClient;

RPNClient client = RPNClient.create(
    // You'll need to identify the user the current user and fetch their access token
    user.RPNAccessToken,
    // Change me to LIVE when you're ready to go live
    RPNClient.Environment.SANDBOX
);
var client = RPNClient.Create(
    // We recommend storing your access token in an
    // configuration setting for security, but you could
    // include it as a string directly in your code
    ConfigurationManager.AppSettings["RPNAccessToken"],
    // Change me to LIVE when you're ready to go live
    RPNClient.Environment.SANDBOX
);
Checking a user’s verification status

You’ve just taken your user through the OAuth flow, getting an access token which you can use to manage their account. Now, if appropriate, you should send them to the onboarding flow so we can gather more information about them which we need before they can receive payouts.

Most of your users will have signed up for their RPN account in the OAuth flow itself, so they’ll need to get verified. However, some may have connected existing RPN accounts, in which case they won’t need to be verified again. To decide what to do, you should query your user’s verification status.

Your user’s verification status is exposed in the Creditors API through the verification_status attribute. You can query it like this:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

// The Creditors API returns a list of creditors, but any RPN account connected
// to your application will have a single creditor, so you can just look at the first one
$creditor = $client->creditors()->list()->records[0];

// We'll see below how to redirect users to the onboarding flow, and how it works
if ($creditor->verification_status == "in_review") {
    redirectToOnboardingFlow();
}
import RPN_pro

client = RPN_pro.Client(
    access_token=current_user.RPN_access_token,
    environment='sandbox'
)

# The Creditors API returns a list of creditors, but any RPN account connected
# to your application will have a single creditor, so you can just look at the first one
creditor = client.creditors.list().records[0];

# We'll see below how to redirect users to the onboarding flow, and how it works
if creditor.verification_status == "action_required":
    redirect_to_onboarding_flow()
require 'RPN_pro'

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
  environment: :sandbox
)

# The Creditors API returns a list of creditors, but any RPN account connected
# to your application will have a single creditor, so you can just look at the first one
creditor = client.creditors.list.records.first

# We'll see below how to redirect users to the onboarding flow, and how it works
redirect_to_onboarding_flow if creditor.verification_status == "action_required"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.Creditor;

RPNClient client = RPNClient
    .newBuilder(CurrentUser.RPNAccessToken)
    .withEnvironment(RPNClient.Environment.SANDBOX)
    .build();

// The Creditors API returns a list of creditors, but any RPN account connected
// to your application will have a single creditor, so you can just look at the first one
Creditor creditor = client.creditors().list().execute()[0];

// We'll see below how to redirect users to the onboarding flow, and how it works
if (creditor.getVerificationStatus() == "action_required") {
  redirectToOnboardingFlow();
}
RPNClient client = RPNClient.Create(
    // We recommend storing your access token in an
    // configuration setting for security, but you could
    // include it as a string directly in your code
    ConfigurationManager.AppSettings["RPNAccessToken"],
    // Change me to LIVE when you're ready to go live
    RPNClient.Environment.SANDBOX
);

var creditorListResponse = await RPN.Creditors.ListAsync(
    new RPN.Services.CreditorListRequest() { Limit = 1 }
);

// The Creditors API returns a list of creditors, but any RPN account connected
// to your application will have a single creditor, so you can just look at the first one
RPN.Resources.Creditor creditor = creditorListResponse.Creditors[0];

if (creditor.VerificationStatus == CreditorVerificationStatus.ActionRequired)
{
    // We'll see below how to redirect users to the onboarding flow, and how it works
    RedirectUserToOnboardingFlow();
}

A creditor’s verification status can have one of three values:

A creditor’s verification status can have one of three values:

  • successful: the creditor’s account is fully verified, and they can receive payouts
  • in_review: the creditor has provided all of the information requested, and it is awaiting review by RPN before they can receive payouts
  • action_required: the creditor needs to provide further information to verify their account so they can receive payouts, and should visit the onboarding flow

If your user has just connected to your app and they’re in the action_required state, you should send them to the onboarding flow straight away.

It’s worth noting that your users, after being successfully verified, may later require further verification and go back to the in_review or action_required states. For example, if your user changes the bank account where they receive payouts, we’ll need to check that the new bank account belongs to them before they can receive payouts, so they’ll revert to the action_required state.

We’d suggest including appropriate cues in your UI if your users aren’t in the successful state, letting them know that they won’t receive payouts, and directing them to the onboarding flow if they’re in the action_required state.

You probably won't want to poll the Creditors API on every pageload for your user's verification_status. We'd recommend caching this and refreshing it regularly, as well as at times when it makes sense in your user journey (for example when a user returns to your app through the post-onboarding URL).

Sending your user to the onboarding flow

RPN provides a dedicated onboarding flow where you can send your users to go through the verification process.

Once they’ve finished providing the information required, the UI will link your user directly back to your product, using the “Post-onboarding URL” specified on your app.

To start the onboarding and verification process, send your user to:

When your user lands back in your product, it’s likely that their verification status will not yet be successful - some of the information provided by users requires review by RPN, so it may be a little while before they move to the successful state, or in some cases further information will be requested, meaning they’ll move back to action_required.

Did you find this page helpful? Yes No

Getting started

Setting up Direct Debit mandates

You’ll need to provide a way for your users to set up Direct Debit mandates with their customers (whom we’ll refer to from now on as “end customers”) so they can start collecting payments from them.

A mandate allows you to pull money from an end customer’s bank account with a simple API call.

Two ways to add customers

You have two options for setting up Direct Debit mandates:

  • Use our secure, hosted payment pages: RPN provides secure, hosted, mobile-friendly, conversion-optimised payment pages that have been translated into many European languages and comply with the “scheme rules” of the Direct Debit schemes. You redirect the customer to us, they provide their bank details on our site, and then we send them back to your application. You never have to handle end customers’ bank details.
  • Build your own Direct Debit setup process: Rather than use our ready-made flow, you can build your own. If you’d like to do this, please contact our partnerships team who’ll be able to explain the process.

In this guide, we’ll take you through using our hosted payment pages, which we call the “Redirect Flow”. To set up a mandate, you’ll generate a link to the Redirect Flow using the API, and send the customer there where they’ll enter their details.

Setting up Direct Debit mandates

At this point, it’s worth thinking through how your users will want to send their end customers to the Direct Debit setup flow - making this work in a way which suits their workflow and how they use your product will hugely improve their experience. Here are some ideas you might like to consider:

  • In KashFlow, end customers can set up a Direct Debit by clicking a button on their invoice
  • In RPN for Xero, users collecting payments can copy a personalised link to allow a customer to set up a Direct Debit, ready to paste into their own emails or add as a link on their website
  • In the RPN Dashboard, users collecting payments can send an email to a customer in one click with their own customised copy, asking them to set up a Direct Debit, either for a single customer or for tens or hundreds at a time

You can read some of our suggestions on the best ways for your users to send their end customers to the Direct Debit setup flow in our user experience guide.

Whatever the experience looks like for setting up mandates from your user’s point of view, the code for generating the link looks identical:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    // You'll need to identify the user that the customer is paying and fetch their
    // access token
    'access_token' => $user->RPNAccessToken,
    // Change me to LIVE when you're ready to go live
    'environment' => \RPNPro\Environment::SANDBOX
]);

$redirectFlow = $client->redirectFlows()->create([
    "params" => [
        // A description of what the Direct Debit is for to be shown to the customer
        "description" => "Automatic invoice payments to Acme plc",
        // A unique token for the customer's session
        "session_token" => "dummy_session_token",
        // The URL for a success page you host, to send the customer to when they finish
        "success_redirect_url" => "https://developer.pymnt.us/example-redirect-uri/",
        // Optionally, prefill customer details on the payment page
        "prefilled_customer" => [
            "given_name" => "Tim",
            "family_name" => "Rogers",
            "email" => "tim@pymnt.us",
            "address_line1" => "338-346 Goswell Road",
            "city" => "London",
            "postal_code" => "EC1V 7LQ"
        ]
    ]
]);

// You'll need to redirect the end customer to this URL.
print("URL: " . $redirectFlow->redirect_url);
import os
import RPN_pro

client = RPN_pro.Client(
    # You'll need to identify the user that the customer is paying and fetch their
    # access token
    access_token=user.RPN_access_token,
    # Change this to 'live' when you are ready to go live.
    environment='sandbox'
)

redirect_flow = client.redirect_flows.create(
    params={
        # A description of what the Direct Debit is for to be shown to the customer
        "description" : "Automatic invoice payments to Acme plc",
        # A unique token for the customer's session
        "session_token" : "dummy_session_token",
        # The URL for a success page you host, to send the customer to when they finish
        "success_redirect_url" : "https://developer.pymnt.us/example-redirect-uri/",
        # Optionally, prefill customer details on the payment page
        "prefilled_customer": {
            "given_name": "Tim",
            "family_name": "Rogers",
            "email": "tim@pymnt.us",
            "address_line1": "338-346 Goswell Road",
            "city": "London",
            "postal_code": "EC1V 7LQ"
        }
    }
)

# You'll need to redirect the end customer to this URL.
print("URL: {} ".format(redirect_flow.redirect_url))
require 'RPN_pro'

client = RPNPro::Client.new(
  # You'll need to identify the user that the customer is paying and fetch their
  # access token
  access_token: user.RPN_access_token,
  # Remove the following line when you're ready to go live
  environment: :sandbox
)

redirect_flow = client.redirect_flows.create(
  params: {
    # A description of what the Direct Debit is for to be shown to the customer
    description: 'Automatic invoice payments to Acme plc',
    # A unique token for the customer's session
    session_token: 'dummy_session_token',
    # The URL for a success page you host, to send the customer to when they finish
    success_redirect_url: 'https://developer.pymnt.us/example-redirect-uri/',
    # Optionally, prefill customer details on the payment page
    prefilled_customer: {
      given_name: 'Tim',
      family_name: 'Rogers',
      email: 'tim@pymnt.us',
      address_line1: '338-346 Goswell Road',
      city: 'London',
      postal_code: 'EC1V 7LQ'
    }
  }
)

# You'll need to redirect the end customer to this URL.
puts "URL: #{redirect_flow.redirect_url}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.RedirectFlow;

RPNClient client = RPNClient
    // You'll need to identify the user that the customer is paying and fetch their
    // access token
    .newBuilder(CurrentUser.RPNAccessToken)
    // Change me to LIVE when you're ready to go live
    .withEnvironment(RPNClient.Environment.SANDBOX)
    .build();

RedirectFlow redirectFlow = client.redirectFlows().create()
    // A description of what the Direct Debit is for to be shown to the customer
    .withDescription("Automatic invoice payments to Acme plc")
    // A unique token for the customer's session
    .withSessionToken("dummy_session_token")
    // The URL for a success page you host, to send the customer to when they
    // finish
    .withSuccessRedirectUrl("https://developer.pymnt.us/example-redirect-uri/")
    // Optionally, prefill customer details on the payment page
    .withPrefilledCustomerGivenName("Tim")
    .withPrefilledCustomerFamilyName("Rogers")
    .withPrefilledCustomerEmail("tim@pymnt.us")
    .withPrefilledCustomerAddressLine1("338-346 Goswell Road")
    .withPrefilledCustomerCity("London")
    .withPrefilledCustomerPostalCode("EC1V 7LQ")
    .execute();

// You'll need to redirect the end customer to this URL.
System.out.println(redirectFlow.getRedirectUrl());
var redirectFlowResponse = await client.RedirectFlows.CreateAsync(new RedirectFlowCreateRequest
{
    Description = "Cider Barrels",
    SessionToken = "dummy_session_token",
    SuccessRedirectUrl = "https://developer.pymnt.us/example-redirect-uri/",
    // Optionally, prefill customer details on the payment page
    PrefilledCustomer = new RedirectFlowCreateRequest.RedirectFlowPrefilledCustomer
    {
        GivenName = "Tim",
        FamilyName = "Rogers",
        Email = "tim@pymnt.us",
        AddressLine1 = "338-346 Goswell Road",
        City = "London",
        PostalCode = "EC1V 7LQ"
    }
});

var redirectFlow = redirectFlowResponse.RedirectFlow;

// Hold on to this ID - you'll need it when you
// "confirm" the redirect flow later
Console.WriteLine(redirectFlow.Id);
Console.WriteLine(redirectFlow.RedirectUrl);

You’ll need to customise this code snippet with a few details of your own:

  • Substitute in your user’s RPN access token, which you stored when they completed the OAuth Flow
  • Provide a helpful description which will be shown to the end customer, explaning what their payments are for (e.g. “Automatic invoice payments to Acme plc” or “Monthly membership payments to Peckham Pikes Swimming Club”)
  • Set the success_redirect_url to where you want us to send the customer once they’ve finished the Direct Debit setup flow (you’ll need to make an API call from this page to “complete” the redirect flow - this will be explained below)
  • Pass in a session_token which identifies the end customer’s session on your website (for example their session ID in your application). You provide this when creating the redirect flow, and must provide it again when “completing” it at the end. Supplying this token twice makes sure that the person who completed the redirect flow is the person you sent to it.
  • Optionally, you can include a prefilled_customer object with your end customer’s details. These will be pre-filled on the payment page so your customer doesn’t have to enter them. For a full list of the details you can provide, head to the API reference.

You’ll get back a URL which will look something like https://pay-sandbox.pymnt.us/flow/RE00006GBDVVP3BBCP9Q5318ZVJWE0DN - redirect the end customer to this link where they’ll be able to enter their details and set up their mandate.

Links to the Redirect Flow will expire after a few minutes - this means that you can't include it directly in emails to your user's customers or give it to your users to copy and paste, but rather, must link to a page on your server which will generate a link on the fly.

While you’re testing the mandate setup process in the sandbox, you can use the following sample bank details:

  • In the UK, use the sort code 200000 and the account number 55779911
  • In Sweden, use the clearingnummer (branch code) 5491, the kontonummer (account number) 0000003 and the personnummer (Swedish identity number) 198112289874
  • In Denmark, use the registreringsnummer (bank code) 345, the kontonummer (account number) 3179681 and the CPR-nummer (Danish identity number) 0101701234
  • In Australia, use the BSB 082-082 and the account number 012345678
  • Everywhere else, use the French IBAN FR1420041010050500013M02606

When you’re building an integration with the API, there are some common paths you should make sure your integration handles successfully, for example a customer cancelling their mandate or a payment failing due to lack of funds. We’ll look at handling these cases later.

In the sandbox environment, we provide scenario simulators which allow you to manually trigger certain cases (like a customer cancelling their mandate or a payment failing due to insufficient funds) from the Dashboard so you can test how your integration responds.

Completing the redirect flows

Once the end customer finishes filling out our payment pages, they’ll be redirected to your success_redirect_url. On this page, you’ll need to use a second API call to “complete” the redirect flow. You’ll need the ID of the redirect flow, passed to you in the redirect_flow_id query parameter, plus the session_token we set earlier:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    // You'll need to identify the user that the customer is paying and fetch their
    // access token - you might store this in the end customer's session, for example
    'access_token' => $user->RPNAccessToken,
    // Change me to LIVE when you're ready to go live
    'environment' => \RPNPro\Environment::SANDBOX
]);

$redirectFlow = $client->redirectFlows()->complete(
    $_GET['redirect_flow_id'], // The value of the `redirect_flow_id` query parameter
    ["params" =>
        ["session_token" => "dummy_session_token"] // The session token specified earlier
    ]
);

// Store the mandate ID against the customer's database record so you can charge
// them in future
print("Mandate: " . $redirectFlow->links->mandate . "<br />");
print("Customer: " . $redirectFlow->links->customer . "<br />");

// Display a confirmation page to the customer, telling them their Direct Debit has been
// set up. You could build your own, or use ours, which shows all the relevant
// information and is translated into all the languages we support.
print("Confirmation URL: " . $redirectFlow->confirmation_url . "<br />");
import os
import RPN_pro

client = RPN_pro.Client(
    # You'll need to identify the user that the customer is paying and fetch their access
    # token - you might store this in the end customer's session, for example
    access_token=user.RPN_access_token,
    #Change this to 'live' when you are ready to go live.
    environment='sandbox'
)

redirect_flow = client.redirect_flows.complete(
    "RE00006GBASX53T7KYWT4051FMC0TZA6", # The value of the `redirect_flow_id` query parameter
    params={
        "session_token": "dummy_session_token" // The session token you specified earlier
})

# Store the mandate ID against the customer's database record so you can charge them in
# future
print("Mandate: {}".format(redirect_flow.links.mandate))
print("Customer: {}".format(redirect_flow.links.customer))

# Display a confirmation page to the customer, telling them their Direct Debit has been
# set up. You could build your own, or use ours, which shows all the relevant
# information and is translated into all the languages we support.
print("Confirmation URL: {}".format(redirect_flow.confirmation_url))
require 'RPN_pro'

client = RPNPro::Client.new(
  # You'll need to identify the user that the customer is paying and fetch their access
  # token - you might store this in the end customer's session, for example
    access_token: user.RPN_access_token,
    environment: :sandbox
)

redirect_flow = client.redirect_flows.complete(
    params['redirect_flow_id'], # The value of the `redirect_flow_id` query parameter
    params: { session_token: 'dummy_session_token' }) # The session token you specified earlier

# Store the mandate ID against the customer's database record so you can charge them in
# future
puts "Mandate: #{redirect_flow.links.mandate}"
puts "Customer: #{redirect_flow.links.customer}"

# Display a confirmation page to the customer, telling them their Direct Debit has been
# set up. You could build your own, or use ours, which shows all the relevant
# information and is translated into all the languages we support.
puts "Confirmation URL: #{redirect_flow.confirmation_url}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.RedirectFlow;

RPNClient client = RPNClient
    // You'll need to identify the user that the customer is paying and fetch their
    // access token - you might store this in the end customer's session, for example
    .newBuilder(user.RPNAccessToken)
    // Change me to LIVE when you're ready to go live
    .withEnvironment(RPNClient.Environment.SANDBOX)
    .build();

RedirectFlow redirectFlow = client.redirectFlows()
        .complete("RE00007201VQ3H3HSTM2V02BYG4DPF1S")
                  // The value of the `redirect_flow_id` query parameter
        .withSessionToken("dummy_session_token")
                          // The session token you specified earlier
        .execute();

// Store the mandate ID against the customer's database record so you can charge
// them in future
System.out.println(redirectFlow.getLinks().getMandate());
System.out.println(redirectFlow.getLinks().getCustomer());

// Display a confirmation page to the customer, telling them their Direct Debit has been
// set up. You could build your own, or use ours, which shows all the relevant
// information and is translated into all the languages we support.
System.out.println(redirectFlow.getConfirmationUrl());
var redirectFlowResponse = await client.RedirectFlows
    .CompleteAsync("RE00007201VQ3H3HSTM2V02BYG4DPF1S",
        new RedirectFlowCompleteRequest
        {
            SessionToken = "dummmy_session_token"
        }
    );

var redirectFlow = redirectFlowResponse.RedirectFlow;

// Store the mandate ID against the customer's database record so you can charge
// them in future
Console.WriteLine($"Mandate: {redirectFlow.Links.Mandate}");
Console.WriteLine($"Customer: {redirectFlow.Links.Customer}");

// Display a confirmation page to the customer, telling them their Direct Debit has been
// set up. You could build your own, or use ours, which shows all the relevant
// information and is translated into all the languages we support.
Console.WriteLine($"Confirmation URL: {redirectFlow.ConfirmationUrl}");

The complete method returns the redirect flow object, now including details of the mandate, customer and customer bank account that have been created.

You’ll want to store the mandate ID in your database, associated with the end customer (for example, if you’re building an product for managing memberships to a sports club, you’d store it in the database attached to the database record representing the member). We’d suggest storing the mandates in a separate database table so you can more easily keep track of mandates over their whole lifecycle and handle multiple mandates per customer - we’ll learn more about that later.

With the redirect flow completed and the mandate ID stored, show the end customer a clear confirmation page telling them that their Direct Debit has been set up, and what will happen next.

You could build your own (we’ve included some more thoughts on this in our user experience guide), or you could use our pre-made one which comes ready translated into all the languages we support - just redirect the customer to the URL found in the confirmation_url attribute of the redirect flow. Our provided confirmation page is only available for 15 minutes from the moment when you complet

Our provided confirmation page is only available for 15 minutes from the moment when you complete the redirect flow via the API, so you should redirect the customer straight to it.

Did you find this page helpful? Yes No

Getting started

Staying up-to-date with webhooks

Once you’ve set up mandates, you’ll need to stay up to date as they change. For example, a customer can cancel their Direct Debit at any time by contacting their bank, and you’ll want to reflect this in your product.

To help you handle this, RPN will send you webhooks whenever something happens in one of your users’ RPN accounts. Adding support for webhooks allows you to receive real-time notifications, so you can take automated actions in response, for example:

  • When a customer cancels their mandate with the bank, suspend their club membership
  • When a payment fails due to lack of funds, mark their invoice as unpaid
  • When a customer’s subscription generates a new payment, log it in their “past payments” list

Let’s look at how to build a simple webhook handler. This important foundational work will help us later to take action when things happen to our mandates and payments.

Setting your webhook URL

To start experimenting with webhooks, your application will need to be accessible from the internet so RPN can reach it with HTTP requests. If you’re working locally, the easiest way to do this is with ngrok.

Download it and follow the instructions on the page to install it (you’ll need to unzip the archive, and then make the ngrok binary available in your PATH).

Now, just run ngrok http -bind-tls=true , where is the port where your web server is running (for example ngrok http http -bind-tls=true 8000 if you’re running on port 8000). You’ll see the externally-accessible URL that forwards to your localhost - copy it to your clipboard.

In the sandbox, you can use an HTTP URL for your webhooks, but to go live, the endpoint must be HTTPS secured.

To start receiving webhooks, you’ll need to add this as your app’s webhook URL in the Dashboard here.

Simply enter the HTTPS URL from earlier and add on the path where your webhook handler will be available, then click “Update App”. Next, copy the secret from the “Webhook Details” section which will appear for your app.

Building your first webhook handler

Let’s get started with building a framework for handling webhooks - it’ll handle the webhook event RPN sends you when a customer cancels their mandate.

Webhooks are HTTP POST requests made to the URL you provided, with a JSON body, looking something like this:

POST https://example.com/webhooks HTTP/1.1
User-Agent: RPN-webhook-service/1.1
Content-Type: application/json
Webhook-Signature: 78e3507f61f141046969c73653402cb50b714f04322da04d766ee0f6d2afe65f

{
  "events": [
    {
      "id": "EV123",
      "created_at": "2014-08-04T12:00:00.000Z",
      "action": "cancelled",
      "resource_type": "mandates",
      "links": {
        "mandate": "MD123",
        "organisation": "OR123"
      },
      "details": {
        "origin": "bank",
        "cause": "bank_account_disabled",
        "description": "Your customer closed their bank account.",
        "scheme": "bacs",
        "reason_code": "ADDACS-B"
      }
    }
  ]
}

The body contains one or more events, specifying the type of the resource affected (e.g. “mandates” or “payments”), its ID, the ID of the organisation the resource belongs to (which you stored when obtaining their access token) and the action that occurred (e.g. “cancelled” or “expired”).

The first step to take when you receive a webhook is to check its signature - this makes sure that is genuinely from RPN and hasn’t been forged. A signature is provided in the Webhook-Signature header of the request. We just need to compute the signature ourselves using the POST ed JSON and the webhook endpoint’s secret (which we copied earlier), and compare it to the one in the header.

If they match, the webhook is genuine, because only you and RPN know the secret. It’s important that you keep the secret safe and private.

We can verify the signature like this:

<?php
// We recommend storing your webhook endpoint secret in an environment variable
// for security, but you could include it as a string directly in your code
$webhook_endpoint_secret = getenv("RPN_WEBHOOK_ENDPOINT_SECRET");
$request_body = file_get_contents('php://input');

$headers = getallheaders();
$signature_header = $headers["Webhook-Signature"];

try {
    $events = \RPNPro\Webhook::parse($request_body,
                                            $signature_header,
                                            $webhook_endpoint_secret);

    // Process the events...

    header("HTTP/1.1 204 No Content");
} catch(\RPNPro\Core\Exception\InvalidSignatureException $e) {
    header("HTTP/1.1 498 Invalid Token");
}
# Here, we're using a Django view, but essentially the same code will work in
# other Python web frameworks (e.g. Flask) with minimal changes
import json
import hmac
import hashlib
import os

from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import HttpResponse

# Handle the incoming Webhook and check its signature.
class WebhookHandler(View):
    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super(Webhook, self).dispatch(*args, **kwargs)

    def is_valid_signature(self, request):
        secret = bytes(os.environ['GC_WEBHOOK_SECRET'], 'utf-8')
        computed_signature = hmac.new(
            secret, request.body, hashlib.sha256).hexdigest()
        provided_signature = request.META["HTTP_WEBHOOK_SIGNATURE"]
        # In flask, access the webhook signature header with
        # request.headers.get('Webhook-Signature')
        return hmac.compare_digest(provided_signature, computed_signature)

    def post(self, request, *args, **kwargs):
        if self.is_valid_signature(request):
            return HttpResponse(200)
        else:
            return HttpResponse(498)
# Here, we're using a Rails controller, but essentially the same code will work in other
# Ruby web frameworks (e.g. Sinatra) with minimal changes

class WebhooksController < ApplicationController
  include ActionController::Live

  protect_from_forgery except: :create

  def create
    # We recommend storing your webhook endpoint secret in an environment variable
    # for security, but you could include it as a string directly in your code
    webhook_endpoint_secret = ENV['RPN_WEBHOOK_ENDPOINT_SECRET']

    # In a Rack app (e.g. Sinatra), access the POST body with
    # `request.body.tap(&:rewind).read`
    request_body = request.raw_post

    # In a Rack app (e.g. Sinatra), the header is available as
    # `request.env['HTTP_WEBHOOK_SIGNATURE']`
    signature_header = request.headers['Webhook-Signature']

    begin
      events = RPNPro::Webhook.parse(request_body: request_body,
                                            signature_header: signature_header,
                                            webhook_endpoint_secret: webhook_endpoint_secret)

      # Process the events...

      render status: 204, nothing: true
    rescue RPNPro::Webhook::InvalidSignatureError
      render status: 498, nothing: true
    end
  end
end
package hello;

// Use the POM file at
// https://raw.githubusercontent.com/RPN/RPN-pro-java-maven-example/master/pom.xml

import com.RPN.errors.InvalidSignatureException;
import com.RPN.resources.Event;
import com.RPN.Webhook;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;

@RestController
public class HelloController {
    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signatureHeader,
            @RequestBody String requestBody) {
        String webhookEndpointSecret = System.getenv("RPN_WEBHOOK_ENDPOINT_SECRET");

        try {
            List<Event> events = Webhook.parse(requestBody, signatureHeader, webhookEndpointSecret)

            for (Event event : events) {
                // Process the event
            }

            return new ResponseEntity<String>("OK", HttpStatus.OK);
        } catch(InvalidSignatureException e) {
            return new ResponseEntity<String>("Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}
[HttpPost]
public ActionResult HandleWebhook()
{
    var requestBody = Request.InputStream;
    requestBody.Seek(0, System.IO.SeekOrigin.Begin);
    var requestJson = new StreamReader(requestBody).ReadToEnd();

    // We recommend storing your webhook endpoint secret in an environment variable
    // for security, but you could include it as a string directly in your code
    var secret = ConfigurationManager.AppSettings["RPNWebhookSecret"];

    var hmac256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    byte[] messageHash = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestJson));
    var result = BitConverter.ToString(messageHash).Replace("-", "").ToLower();

    // If the signature doesn't match what was expected, reject the request
    if ((Request.Headers["Webhook-Signature"] ?? "") != result)
      return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

    // Otherwise, handle the events in the webhook and respond with 200 OK
    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

Now, let’s add in some basic logic to process mandate cancellations (where resource_type is mandates and action is cancelled). Here, you’ll:

  • Calculate the correct signature using our secret and the request body, and compare that to the one in the headers
  • For each event in the body:
    • Look up the ID in your database to make sure you haven’t processed it before. You’ll need to keep a record to make sure you only process each event once.
    • Enqueue a job to asynchronously perform any actions you want to perform (e.g. emailing your user to let them know about the cancellation)
    • Record in the database that you’ve processed the event
  • Respond with a successful response (204 No Content). If you respond with an HTTP error code, or the request from RPN to your server times out, RPN will retry up to ten times at increasing intervals.
<?php
function process_mandate_event($event, $client)
{
    $mandate = $client->mandates()->get($event->links["mandate"]);

    switch ($event->action) {
    case "cancelled":
        print("Mandate " . $mandate->id . " has been cancelled!\n");

        // You should perform processing steps asynchronously to avoid timing out if
        // you receive many events at once. To do this, you could use a queueing
        // system like Beanstalkd
        //
        // http://george.webb.uno/posts/sending-php-email-with-mandrill-and-beanstalkd
        CancelServiceAndNotifyCustomer::performAsynchronously($mandate->id);
        RPNEvent::create(['RPN_id' => $event->id]);
        break;
    default:
        print("Don't know how to process a mandate " . $event->action . " event\n");
        break;
  }
}

function is_already_processed($event)
{
    return RPNEvent::where(['RPN_id' => $event->id])->count() > 0;
}

// We recommend storing your webhook endpoint secret in an environment variable
// for security, but you could include it as a string directly in your code
$webhook_endpoint_secret = getenv("RPN_WEBHOOK_ENDPOINT_SECRET");
$request_body = file_get_contents('php://input');

$headers = getallheaders();
$signature_header = $headers["Webhook-Signature"];

try {
    $events = \RPNPro\Webhook::parse($request_body,
                                            $signature_header,
                                            $webhook_endpoint_secret);

    // Each webhook may contain multiple events to handle, batched together
    foreach ($events as $event) {
        print("Processing event " . $event->id . "\n");

        $organisation_id = $event->links["organisation"];
        $access_token = User::where(['RPN_organisation_id' => $organisation_id])
          ->firstOrFail()
          ->RPN_access_token;

        $client = new \RPNPro\Client([
            'access_token' => $access_token,
            'environment' => \RPNPro\Environment::SANDBOX
        ]);

        if (is_already_processed($event)) {
            continue;
        }

        switch ($event->resource_type) {
        case "mandates":
            process_mandate_event($event, $client);
            break;
        default:
            print("Don't know how to process an event with resource_type " . $event->resource_type . "\n");
            break;
      }
    }

    header("HTTP/1.1 204 No Content");
} catch(\RPNPro\Core\Exception\InvalidSignatureException $e) {
    header("HTTP/1.1 498 Invalid Token");
}
# Here, we're using a Django view, but essentially the same code will work in
# other Python web frameworks (e.g. Flask) with minimal changes

import json
import hmac
import hashlib
import os
import logging

from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import HttpResponse

from myinvoicingapp.models import RPNEvent
from myinvoicingapp.models import User

# Handle the incoming Webhook and perform an action with the  Webhook data.
class WebhookHandler(View):
    logger = logging.getLogger(__name__)

    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super(Webhook, self).dispatch(*args, **kwargs)

    def is_valid_signature(self, request):
        secret = bytes(os.environ['GC_WEBHOOK_SECRET'], 'utf-8')
        computed_signature = hmac.new(
            secret, request.body, hashlib.sha256).hexdigest()
        provided_signature = request.META["HTTP_WEBHOOK_SIGNATURE"]
        return hmac.compare_digest(provided_signature, computed_signature)

    def post(self, request, *args, **kwargs):
        if self.is_valid_signature(request):
            payload = json.loads(request.body.decode('utf-8'))

            # Each webhook may contain multiple events to handle, batched together.
            for event in payload['events']:
                self.process(event)
            return HttpResponse(204)
        else:
            return HttpResponse(498)

    def is_already_processed(event):
        return RPNEvent.objects.filter(RPN_id=event['id']).exists()

    def process(self, event):
        logger.info("Processing event {}\n".format(event['id']))

        if is_already_processed(event):
            return

        if event['resource_type'] == 'mandates':
            return self.process_mandate_event(event)
        # ... Handle other resource types
        else:
            logger.info("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))

    def process_mandate_event(self, event):
        organisation_id = event['links']['organisation']

        access_token = User.objects.get(RPN_organisation_id=organisation_id).RPN_access_token

        client = RPN_pro.Client(
            access_token=access_token,
            environment='sandbox'
        )

        mandate = client.mandates.get(event['id'])

        if event['action'] == 'cancelled':
            # You should perform processing steps asynchronously to avoid timing out
            # if you receive many events at once. To do this, you could use a
            # queueing system like RQ (https://github.com/ui/django-rq)
            logger.info("Mandate {} has been cancelled\n".format(mandate.id))
        # ... Handle other mandate actions
        else:
            logger.info("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))

        RPNEvent.objects.create(RPN_id=mandate.id)

        return response
require 'RPN_pro'

class MandateEventProcessor
  def self.process_mandate_event(event)
    return if already_processed?(event)

    organisation_id = event.links.organisation

    access_token = User.find_by(RPN_organisation_id: organisation_id).access_token
    RPN = RPNPro::Client.new(access_token: access_token,
                                           environment: :sandbox)
    mandate = RPN.mandates.get(event.links.mandate)

    case event.action
    when 'cancelled'
      Rails.logger.info("Mandate #{mandate.id} has been cancelled\n")

      # You should perform processing steps asynchronously to avoid timing out
      # if you receive many events at once. To do this, you could use a
      # queueing system like Resque (https://github.com/resque/resque)
      CancelServiceAndNotifyCustomer.enqueue(event.links.mandate)

      RPNEvent.create!(RPN_id: event.id)
    else
      Rails.logger.info("Don't know how to process a mandate #{event.action} event\n")
    end

    def self.already_processed?(event)
      RPNEvent.where(RPN_id: event.id).any?
    end
  end
end

def create
  # We recommend storing your webhook endpoint secret in an environment variable
  # for security, but you could include it as a string directly in your code
  webhook_endpoint_secret = ENV['RPN_WEBHOOK_ENDPOINT_SECRET']

  # In a Rack app (e.g. Sinatra), access the POST body with
  # `request.body.tap(&:rewind).read`
  request_body = request.raw_post

  # In a Rack app (e.g. Sinatra), the header is available as
  # `request.env['HTTP_WEBHOOK_SIGNATURE']`
  signature_header = request.headers['Webhook-Signature']

  begin
    events = RPNPro::Webhook.parse(request_body: request_body,
                                          signature_header: signature_header,
                                          webhook_endpoint_secret: webhook_endpoint_secret)

    events.each do |event|
      case event.resource_type
      when 'mandates'
        MandateEventProcessor.process(event)
      else
        Rails.logger.info("Don't know how to process an event with resource_type " \
                          "#{event.resource_type}\n")
      end
    end

    render status: 204, nothing: true
  rescue RPNPro::Webhook::InvalidSignatureError
    render status: 498, nothing: true
  end
end
package com.myInvoicingApp;

// Use the POM file at
// https://raw.githubusercontent.com/RPN/RPN-pro-java-maven-example/master/pom.xml

import com.RPN.resources.Event;
import com.myInvoicingApp.RPNEventLog;
import com.myInvoicingApp.MandateWebhookHandler;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebhookHandler {
    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signatureHeader,
            @RequestBody String requestBody) {
        try {
            String webhookEndpointSecret = System.getenv("RPN_WEBHOOK_ENDPOINT_SECRET");

            List<Event> events = Webhook.parse(requestBody, signatureHeader, webhookEndpointSecret)

            String responseBody = "";

            for (Event event : events) {
                responseBody += processEvent(event);
            }

            return new ResponseEntity<String>(responseBody, HttpStatus.OK);
        } catch(InvalidSignatureException e) {
            return new ResponseEntity<String>("Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }

    private String processEvent(Event event) {
        if RPNEventLog.alreadyProcessed(event.getId()) {
          return "Event already processed";
        }
        switch (event.getResourceType()) {
            case MANDATES:
                return processMandateEvent(event);
            default:
                return "Don't know how to process an event of resource_type " +
                    event.getResourceType().toString() + ".\n";
        }
    }

    private String processMandateEvent(Event event) {
    /*
      You should keep some kind of record of what events have been processed
      to avoid double-processing, checking if the event already exists
      before processing it.

      You should perform processing steps asynchronously to avoid timing out
      if you receive many events at once. To do this, you could use a
      queueing system like @Async.

      https://spring.io/guides/gs/async-method/

      Once you've performed the actions you want to perform for an event, you
      should make a record to avoid accidentally processing the same one twice
    */
        switch (event.getAction()) {
            case "cancelled":
                MandateWebhookHandler.handleCancelledEvent(event);
                RPNEventLog.save(event.getId());
                return "Mandate " + event.getLinks().getMandate() +
                    " has been cancelled.\n";
            default:
                return "Do not know how to process an event with action " +
                    event.getAction() + ".\n";
        }
    }
}
[HttpPost]
public ActionResult HandleWebhook()
{
    variable requestBody = Request.InputStream;
    requestBody.Seek(0, System.IO.SeekOrigin.Begin);
    var requestJson = new StreamReader(requestBody).ReadToEnd();

    // We recommend storing your webhook endpoint secret in an environment variable
    // for security, but you could include it as a string directly in your code
    var secret = ConfigurationManager.AppSettings["RPNWebhookSecret"];

    var hmac256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    byte[] messageHash = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestJson));
    var result = BitConverter.ToString(messageHash).Replace("-", "").ToLower();

    // If the signature doesn't match what was expected, reject the request
    // Otherwise, handle the events in the webhook then respond with 200 OK
    if ((Request.Headers["Webhook-Signature"] ?? "") != result)
      return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

    // Here we deserialize the webhook using the same objects the library uses to
    // represent API responses for Events, but you could parse and handle the JSON
    // body however you prefer.
    var serializerSettings = new RPN.Internals.JsonSerializerSettings();
    RPN.Services.EventListResponse events =
        JsonConvert.DeserializeObject<RPN.Services.EventListResponse>(message, serializerSettings);

    // Webhooks can contain many events. In a real implementation, you should handle
    // the processing of each event asychronously to avoid timing out here.
    // You could check whether you've processed the event before (by recording the event
    // ID when you process it) and retrieve the associated resource to ensure that you have
    // the most up-to-date information about it.
    foreach (RPN.Resources.Event eventResource in events.Events)
    {
        // To keep this example simple, we're only handling Mandate events
        if (eventResource.ResourceType == RPN.Resources.EventResourceType.Mandates)
        {
            switch (eventResource.Action)
            {
                case "created":
                    Console.WriteLine($"Mandate {eventResource.Links.Mandate} has been created, yay!");
                    break;
                case "cancelled":
                    Console.WriteLine($"Oh no, mandate {eventResource.Links.Mandate} was cancelled!");
                    break;
                default:
                    Console.WriteLine($"{eventResource.Links.Mandate} has been {eventResource.Action}");
                    break;
            }
        }
        else
        {
            Console.WriteLine($"No method to handle {eventResource.ResourceType} events yet");
        }
    }
    return new HttpStatusCodeResult(HttpStatusCode.OK);
}
Testing your webhook handler

In the Dashboard, the “Send test web hook” functionality makes it easy to start experimenting with webhooks.

Select your app, set the “Resource type” to mandates, the “action” to cancelled, the “cause” and “event details” to whatever you want and enter an organisation ID. Then, click “Send test webhook”.

RPN will make a request to your endpoint, and full details of the request and response will appear in the list - click on it. If everything’s working, you’ll see a response code of 204 No Content.

We’ve now built the basic structure of a webhook handler. In the next section, we’ll look at how to effectively handle the most important parts of the lifecycle of a mandate.

Did you find this page helpful? Yes No

Getting started

Managing mandates

In the previous step, we implemented a simple webhook handler. In this section, we’ll build on that and learn how to handle the full mandate lifecycle by listening for the relevant webhooks, as well as thinking about how to support mandates set up outside of our integration.

In the API reference, you’ll find a full list of all the events that can happen to mandates (and other resources too). We’ll take you through the most important cases here.

You don’t necessarily need to handle all of the possible events: for example, when a mandate is sent to the banks for processing, this doesn’t necessarily need to be reflected in your product, but your users will definitely want to know when a customer cancels. Below, we’ll cover the most important paths to be able to deal with.

Every partner integration is different, so this guide can’t tell you exactly what to do, but we can explain the cases you should handle and give you some ideas of how to provide the best experience for your users.

Mandate cancellations

Mandates can be cancelled at any time. For example, end customers can cancel their mandates by contacting their bank, or your user can cancel a mandate from their RPN Dashboard. When this happens, you’ll receive a webhook with a resource_type of “mandates”, and an action of “cancelled”.

We’ve already written some sample code in the previous step for handling mandate cancellations event.

You should build on that basic framework, thinking about what it makes sense to do in your product in response to a cancellation - for example, you might want to:

  • update your UI, so your user can see that the end customer no longer has an active mandate
  • deactivate the end customer’s access to the service being provided
  • email your user to let them know that no more payments will be collected until a new mandate is set-up

As discussed in the "Setting up Direct Debit mandates" section, you'll most likely want a dedicated table in your database for RPN mandates.

This will help you keep track of mandates and their statuses through the mandate lifecycle, and to link those mandates to an internal customer record, representing the person paying your user.

The details attribute on the event includes an origin, cause and description to help you understand why and how the mandate came to be cancelled - you can see a full list of possible attributes for this and other webhook events in the API reference.

You can also cancel mandates yourself via the API if an end customer should no longer be charged. You should only do this if it’s requested by your user:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

$mandate = $client->mandates()->cancel($mandate_id);
print("Status: " . $mandate->status);
import RPN_pro

def cancel_mandate(current_user, mandate_id):
    client = RPN_pro.Client(
        access_token=current_user.RPN_access_token,
        environment='sandbox'
    )

    mandate = client.mandates.cancel(mandate_id)
    print("Status: {}".format(mandate.status))
require 'RPN_pro'

def cancel_mandate(current_user, mandate_id)
  client = RPNPro::Client.new(
    access_token: current_user.RPN_access_token,
    environment: :sandbox
  )

  mandate = client.mandates.cancel(mandate_id)
  puts "Status: #{mandate.status}"
end
package com.myInvoicingApp;


import com.RPN.RPNClient;
import com.RPN.resources.Mandate;
import com.myInvoicingApp.User;

public class CancelMandate {
    public static void main(User currentUser, String mandateID) {
        RPNClient client = RPNClient
            .newBuilder(CurrentUser.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        Mandate mandate = client.mandates().cancel(mandateID).execute();
        System.out.printf("Status: %s%n", mandate.getStatus());
    }
}
const string mandateId = "MD0000123ABC";
Console.WriteLine($"Cancelling mandate: {mandateId}");

var cancelResponse = await client.Mandates.CancelAsync(mandateId);
var mandate = cancelResponse.Mandate;
Console.WriteLine($"Status: {mandate.Status}");

Mandates can also, in rare cases, expire: when you see the expired action, you’ll probably want to take similar steps as you would for a cancellation.

Mandate failures

Once you’ve created a mandate, it takes a couple of days to be submitted to the bank and fully set-up. In certain cases (for example if the customer’s bank account doesn’t support Direct Debit or the bank account has been closed), the mandate setup will fail, and you’ll receive an event with resource_type “mandates” and action “failed”.

When a mandate fails to be set up, you could consider:

  • emailing the end customer to tell them what went wrong, and suggesting they try again with a different bank account (the cause under the event’s details includes an explanation of what went wrong - either invalid_bank_details, bank_account_closed, bank_account_transferred, direct_debit_not_enabled or other)
  • updating your UI, so your user can see that the end customer no longer has an active mandate

Read more about reporting the status of mandates to your users in our user experience guide.

Mandate replacements

If one of your users switches between the RPN Standard, Plus and Pro packages, they’ll get their own “scheme identifier” - this means that their own name appears on their customers’ bank statements instead of “pymnt.us”.

When this happens, RPN will move their existing mandates from the RPN shared scheme identifier to the user’s own personal one.

For Bacs (UK) mandates, this leads to a change in the mandates’ IDs. You will be alerted by being sent a webhook event with resource_type “mandates” and action “replaced”. The links[new_mandate] attribute will include the ID of the new mandate. If you try to create a payment or subscription against the mandate, you’ll get an error with the “invalid_state” type and the “mandate_replaced” reason, again with a links[new_mandate] attribute.

You should update the mandate ID recorded in your database, as you’ll need to use the new mandate’s ID to charge the customer in the future.

Supporting mandates set up outside of your product

We’ve already looked at setting up new Direct Debits through your product.

To provide the best experience, you should consider allowing your users to “import” mandates set up outside your product so they can bill their customers within your product. There are three reasons why your users might have mandates in this situation:

  • Existing user of RPN: It’s likely that some of your users will have already been using RPN before connecting it to your product, so they may already have Direct Debit mandates set up with their customers.
  • Bulk changes: If your user was previously using Direct Debit through another provider, they, subject to availability, can “bulk change” their existing mandates from their existing provider to RPN.
  • Mixing and matching ways of using RPN: Your users might choose to add customers using our Dashboard, our API (for example on their website) or using another integration to better match with their workflow.

You can support importing mandates in two main ways:

  • Automated imports: Fetch a list of the user’s mandates using the Mandates API, then match the customers’ personal details (given_name, family_name, company_number and email) against your application’s internal records to automatically suggest matches, allowing the user to approve these pairings or manually correct them if incorrect (see below for more detailed instructions)
  • Manual imports: Allow users to export a list of customers from your product as a spreadsheet, fill in the mandate IDs from RPN, and then upload the completed spreadsheet or email it back to you so you can update your internal records

We recommend supporting automated imports - this makes it as easy as possible for your customers to get set up and then get the best value out of your integration, whilst keeping manual administrative work for you to a minimum.

Building automatic mandate matching

Automatically loading and matching existing Direct Debit mandates provides the best customer experience, making importing your users’ existing customers an easy and integrated part of the process of connecting RPN to your product.

We’d suggest building a flow like this:

  • Prepare a database table which links RPN mandates to one of your users, and includes columns for the customer’s given name, family name, company name, email, the RPN mandate ID and the RPN customer ID.
  • Load all mandates using the API, filtering for those in statuses “active”, “submitted” and “pending_submission”.
  • For each of them, load the customer using the customer ID in links[customer], so you have the details for both the mandate and the customer it’s attached to.
  • Automatically try to match the RPN mandates against customer records in your product, for example using the names or email, and record these suggested matches in your database. You’ll need some way of storing in your database suggested matches between RPN mandates and your product’s internal customer records, and what match, if any, your user has picked.
  • Provide a UI where your user can confirm the suggested pairings between mandates and customers, or override the suggestions where they’re incorrect, picking another mandate
  • Use webhooks to track when new mandates are created or existing ones become inactive and then, following steps 3-5 above, record them. This will allow you to support users “bulk changing” mandates to RPN from another Direct Debit provider or adding customers outside of your product throughout the time they’re using your software, rather than this just being a one-off process when the user first connects their RPN account to your product.

In addition to importing existing mandates, you may want to consider querying your user’s RPN account for existing subscriptions, payments, refunds, and payouts to ensure that your product can reflect a complete record of your user’s ongoing Direct Debit activity.

If your user has previously used automatically recurring subscriptions and your integration uses single payment requests (we’ll explain the difference on the next page), you may want to silently cancel the existing subscriptions - for help with this, get in touch with our API support team.

Did you find this page helpful? Yes No

Getting started

Setting up payments

Once you’ve set up Direct Debit mandates with your users’ end customers, you can start collecting payments from them using either automatically-recurring subscriptions or one-off payments.

With the mandate created, you’re ready to go - there’s no need to wait for it to become active, as RPN will automatically submit payments at the right time to charge the end customer as soon as possible.

Two ways to collect payments

There are two ways to set up payments - which one is right for you will depend on the kinds of payments your users want to take, and how you want to manage the process through your product:

Subscriptions
Set up an automatic recurring payment. This works great for users that want to take the same payment on a regular basis (for instance £5 per week, or £20 on the first of each month).

You can add an app_fee on top of RPN's fees or receive a share of RPN's collected fees.

When you setup an app fee, your fee will be taken from every payment created for that subscription.

One-off payments
Trigger a payment against a mandate at any time with the API. This allows your user to charge their end customers ad-hoc amounts.

You can add an app_fee on top of RPN's fees or receive a share of RPN's collected fees.

You can use one-off payments to build your own subscriptions logic, for example if you want to add additional amounts based on usage to a fixed regular payment.

Your integration may either apply app fees or receive a revenue share from RPN's fees. No app can have both app fees and a revenue share. For more information or to set up a revenue share, contact the partnerships team.

Collecting one-off payments

Let’s start by collecting a one-off payment of £10 from an end customer:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

$payment = $client->payments()->create([
  "params" => [
      "amount" => 1000, // 10 GBP in pence collected from end customer
      "app_fee" => 10, // 10 pence, to be paid out to you
      "currency" => "GBP",
      "links" => [
          "mandate" => "MD0000XH9A3T4C" // The mandate ID from last section
      ],
      // Almost all resources in the API let you store custom metadata,
      // which you can retrieve later
      "metadata" => [
          "invoice_number" => "001"
      ]
  ],
  "headers" => [
      "Idempotency-Key" => "random_payment_specific_string"
  ]
]);

// Keep hold of this payment ID - we'll use it in a minute
// It should look like "PM000260X9VKF4"
print("ID: " . $payment->id);
import os
import RPN_pro

client = RPN_pro.Client(
    access_token=current_user.RPN_access_token,
    environment='sandbox'
)

payment = client.payments.create(
    params={
        "amount" : 1000, # 10 GBP in pence collected from end customer.
        "app_fee": 10, # 10 pence, to be paid out to you.
        "currency" : "GBP",
        "links" : {
            "mandate": "MD0000XH9A3T4C"
                     # The mandate ID from the last section
        },
        # Almost all resources in the API let you store custom metadata,
        # which you can retrieve later
        "metadata": {
          "invoice_number": "001"
        }
    }, headers={
        'Idempotency-Key' : 'random_key',
})

# Keep hold of this payment ID - we will use it in a minute
# It should look like "PM000260X9VKF4"
print("ID: {}".format(payment.id))
require 'RPN_pro'

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
    environment: :sandbox
)

payment = client.payments.create(
  params: {
    amount: 1000, # 10 GBP in pence, collected from the end customer.
    app_fee: 10, # 10 pence, to be paid out to you.
    currency: 'GBP',
    links: {
      mandate: 'MD0000XH9A3T4C'
             # The mandate ID from last section
    },
    # Almost all resources in the API let you store custom metadata,
    # which you can retrieve later
    metadata: {
      invoice_number: '001'
    }
  },
  headers: {
    'Idempotency-Key' => 'random_payment_specific_string'
  }
)

# Keep hold of this payment ID - we will use it in a minute
# It should look like "PM000260X9VKF4"
puts "ID: #{payment.id}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.Payment;
import com.RPN.services.PaymentService.PaymentCreateRequest.Currency;

public class SinglePayment {
    public static void main(String[] args) {
        RPNClient client = RPNClient
            .newBuilder(CurrentUser.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        Payment payment = client.payments().create()
            .withAmount(1000) // 10 GBP in pence collected from end customer.
            .withAppFee(10) // 10 pence, to be paid out to you.
            .withCurrency(Currency.GBP)
            .withLinksMandate("MD0000XH9A3T4C")
            .withMetadata("invoice_number", "001")
            .withIdempotencyKey("random_payment_specific_string")
            .execute();
        // Keep hold of this payment ID - we'll use it in a minute
        // It should look like "PM000260X9VKF4"
        System.out.println(payment.getId());
    }
}
var createResponse = await client.Payments.CreateAsync(new PaymentCreateRequest
{
    Amount = 1000,
    Currency = PaymentCreateRequest.PaymentCurrency.GBP,
    Links = new PaymentCreateRequest.PaymentLinks
    {
        Mandate = "MD0000XH9A3T4C",
    },
    Metadata = new Dictionary<string, string>
    {
        {"invoice_number", "001"}
    },
    IdempotencyKey = "random_payment_specific_string"
});

var payment = createResponse.Payment;

// Keep hold of this payment ID - we'll use it in a minute
// It should look like "PM000260X9VKF4"
Console.WriteLine(payment.Id);

You’ll need to use the currency appropriate for the mandate. When an end customer goes through the Redirect Flow, we'll choose the right Direct Debit scheme based on their bank account, and each scheme supports a single currency. You will likely want to use the mandates API to read the mandate's scheme attribute, and translate that into a currency using this table:

scheme currency
bacs GBP
autogiro SEK
betalingsservice DKK
becs AUD
sepa_core EUR

You’ll notice here that we provide an Idempotency-Key header. If we provide a unique string specific to this payment (for example its ID in our own database), the API will ensure this payment is only ever created once.

This means that if an API request times out or something goes wrong on your end, you won’t ever accidentally bill a customer twice - see our blog post for more details. You can use idempotency keys whenever you create something with the API.

In the live environment, we would debit £10.00 from the end customer’s bank account. From that, before paying it out to your user, RPN would deduct its fees, and any app_fee you’ve specified which would be deducted and passed on to you.

Taken from end customer's bank account £10.00
RPN fee £0.20
Your app_fee £0.10
Paid out to your user £9.70

If you're creating payments in bulk (or making many requests in a short period for any other reason), you'll need to make sure you don't exceed our rate limit and handle any 429 Too Many Requests errors appropriately. You can make up to 1000 requests per minute for each of your access tokens (i.e. for each of your users).

You can earn revenue from payments collected through your product either by adding an app_fee to what RPN charges, or by receiving a share of RPN’s fees.

The choice of whether you want to charge an app_fee, and if so, how much, is yours. However, it may not be more than half the value of the payment and must be made clear to your user.

For any questions about app fees, RPN fees or revenue sharing, please don’t hesitate to contact our partnerships team.

Creating a payment returns details on the payment you’ve set up, including its ID. Once you have the ID, you can also perform useful functions on the payment, like cancelling it or retrying it if it has failed.

Let’s try grabbing our payment, and then cancelling it, taking a look at its status before and after:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

$payment = $client->payments()->get("PM000260X9VKF4");
                              // Payment ID from above

print("Status: " . $payment->status . "<br />");
print("Cancelling...<br />");

$payment = $client->payments()->cancel("PM000260X9VKF4");
print("Status: " . $payment->status);
import os
import RPN_pro

client = RPN_pro.Client(
    access_token=current_user.RPN_access_token,
    environment='sandbox'
)

payment = client.payments.get("PM000260X9VKF4")
                         # Payment ID from above.

print("Amount: {}".format(payment.amount))
print("Cancelling...")

payment = client.payments.cancel("PM000260X9VKF4")
print("Status: {}".format(payment.status))
require 'RPN_pro'

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
    environment: :sandbox
)

payment = client.payments.get('PM000269VJAR6M')
                         # Payment ID from above.

puts "Status: #{payment.status}"
puts 'Cancelling...'

payment = client.payments.cancel('PM000269VJAR6M')
puts "Status: #{payment.status}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.Payment;

public class CancelPayment {
    public static void main(String[] args) {
        RPNClient client = RPNClient
            .newBuilder(CurrentUser.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        Payment payment = client.payments().get("PM000260X9VKF4").execute();
                                              // Payment ID from above.
        System.out.printf("Amount: %s%n", payment.getAmount());
        System.out.println("Cancelling...");

        payment = client.payments().cancel("PM000260X9VKF4").execute();
        System.out.printf("Status: %s%n", payment.getStatus());
    }
}
// Payment ID from above
const string paymentId = "PM000260X9VKF4";

var paymentResponse = await client.Payments.GetAsync(paymentId);
var payment = paymentResponse.Payment;

Console.WriteLine($"Amount: {payment.Amount}");
Console.WriteLine("Cancelling...");

var cancelResponse = await client.Payments.CancelAsync(paymentId);
var payment = cancelResponse.Payment;
Console.WriteLine($"Status: {payment.Status}");
Using recurring subscriptions

Now, let’s create a subscription. Subscriptions collect a fixed, regular amount from a customer.

Let’s try collecting £15 per month on the 5th of each month from an end customer:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

$subscription = $client->subscriptions()->create([
    "params" => [
        "amount" => 1500, // 15 GBP in pence, collected from the end customer
        "app_fee" => 10, // 10 pence, to be paid out to you
        "currency" => "GBP",
        "interval_unit" => "monthly",
        "day_of_month" => "5",
        "links" => [
            "mandate" => "MD0000XH9A3T4C"
                         // Mandate ID from the last section
        ],
        "metadata" => [
            "subscription_number" => "ABC1234"
        ]
    ],
    "headers" => [
        "Idempotency-Key" => "random_subscription_specific_string"
    ]
]);

// Keep hold of this subscription ID - we'll use it in a minute.
// It should look a bit like "SB00003GKMHFFY"
print("ID: " . $subscription->id);
import os
import RPN_pro

client = RPN_pro.Client(
    access_token=current_user.RPN_access_token,
    environment='sandbox'
)

subscription = client.subscriptions.create(
    params={
        "amount" : 1500, # 15 GBP in pence, collected from the end customer.
        "app_fee": 10, # 10 pence, to be paid out to you.
        "currency" : "GBP",
        "interval_unit" : "monthly",
        "day_of_month" : "5",
        "links": {
            "mandate": "MD0000XH9A3T4C"
                     # Mandate ID from the last section
        },
        "metadata": {
            "subscription_number": "ABC1234"
        }
    }, headers={
        'Idempotency-Key': "random_subscription_specific_string"
})

# Keep hold of this subscription ID - we'll use it in a minute
# It should look a bit like "SB00003GKMHFFY"
print("ID: {}".format(subscription.id))
require 'RPN_pro'

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
    environment: :sandbox
)

subscription = client.subscriptions.create(
  params: {
    amount: 1500, # 15 GBP in pence, collected from the customer
    app_fee: 10, # 10 pence, to be paid out to you
    currency: 'GBP',
    interval_unit: 'monthly',
    day_of_month: '5',
    links: {
      mandate: 'MD0000XH9A3T4C'
              # Mandate ID from the last section
    },
    metadata: {
      subscription_number: 'ABC1234'
    }
  },
  headers: {
    'Idempotency-Key': 'random_subscription_specific_string'
  }
)

# Keep hold of this subscription ID - we'll use it in a minute
# It should look a bit like "SB00003GKMHFFY"
puts "ID: #{subscription.id}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.Subscription;
import com.RPN.services.SubscriptionService.SubscriptionCreateRequest.IntervalUnit;

public class CreateSubscription {
    public static void main(String[] args) {
        RPNClient client = RPNClient
            .newBuilder(CurrentUser.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        Subscription subscription = client.subscriptions().create()
            .withAmount(1500) // 15 GBP in Pence, collected from the end customer.
            .withAppFee(10) // 10 pence, to be paid out to you.
            .withCurrency("GBP")
            .withIntervalUnit(IntervalUnit.MONTHLY)
            .withDayOfMonth(5)
            .withLinksMandate("MD0000YTKZKY4J")
                             // Mandate ID from the last section
            .withMetadata("subscription_number", "ABC123")
            .withIdempotencyKey("random_subscription_specific_string")
            .execute();

        // Keep hold of this subscription ID - we'll use it in a minute.
        // It should look a bit like "SB00003GKMHFFY"
        System.out.printf("ID: %s%n", subscription.getId());
    }
}
var createResponse = await client.Subscriptions.CreateAsync(new SubscriptionCreateRequest
{
    Amount = 1500,
    AppFee = 10,
    Currency = "GBP",
    Name = "Monthly subscription",
    Interval = 1,
    DayOfMonth = 5,
    IntervalUnit = SubscriptionCreateRequest.SubscriptionIntervalUnit.Monthly,
    Links = new RPN.Services.SubscriptionCreateRequest.SubscriptionLinks
    {
        Mandate = "MD0123"
    },
    IdempotencyKey = "unique_subscription_specific_string"
});

var subscription = createResponse.Subscription;

Console.WriteLine(subscription.Id);

You’ll need to set the currency depending on the customer’s mandate’s scheme.

Each month, on the 5th of the month, the subscription will generate a new payment for £15 to be collected from the end customer.

We will debit £15.00 from the end customer’s bank account. From that, before paying it out to your user, RPN would deduct its fees, and any app_fee you’ve specified which would be deducted and passed on to you.

Taken from end customer's bank account £15.00
RPN fee £0.20
Your app_fee £0.10
Paid out to your user £14.70

With the subscription ID which is returned when we create the subscription, we can grab it from the API.

If you want to stop charging the customer or change the amount they’re being debited, you’ll need to cancel the subscription. Let’s then try doing that via the API so no further payments are taken:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

$subscription = $client->subscriptions()->get("SB00003GKMHFFY");
                                        // Subscription ID from above.

print("Status: " . $subscription->status . "<br />");
print("Cancelling...<br />");

$subscription = $client->subscriptions()->cancel("SB00003GKMHFFY");

print("Status: " . $subscription->status . "<br />");
import os
import RPN_pro

client = RPN_pro.Client(
    access_token=current_user.RPN_access_token,
    environment='sandbox'
)

subscription = client.subscriptions.get("SB00003GKMHFFY")
                                   # Subscription ID from above.

print("Status: {}".format(subscription.status))
print("Cancelling...")

subscription = client.subscriptions.cancel("SB00003GKMHFFY")

print("Status: {}".format(subscription.status))
require 'RPN_pro'

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
    environment: :sandbox
)

subscription = client.subscriptions.get('SB00003GKMHFFY')
                                   # Subscription ID from above.

puts "Status: #{subscription.status}"
puts 'Cancelling...'

subscription = client.subscriptions.cancel('SB00003GKMHFFY')
puts "Status: #{subscription.status}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.Subscription;

public class CancelSubscription {
    public static void main(String[] args) {
        RPNClient client = RPNClient
            .newBuilder(CurrentUser.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        Subscription subscription = client.subscriptions().get("SB00003GKMHFFY").execute();

        System.out.printf("Amount: %s%n", subscription.getStatus());
        System.out.println("Cancelling...");

        subscription = client.subscriptions().cancel("SB00003GKMHFFY").execute();

        System.out.printf("Status: %s%n", subscription.getStatus());
    }
}
// Subscription ID from above
const string subscriptionId = "SB00003GKMHFFY";

var subscriptionResponse = await client.Subscriptions.GetAsync(subscriptionId);
var subscription = subscriptionResponse.Subscription;

Console.WriteLine($"Amount: {subscription.Amount}");
Console.WriteLine("Cancelling...");

var cancelResponse = await client.Subscriptions.CancelAsync(subscriptionId);
subscription = cancelResponse.Subscription;
Console.WriteLine($"Status: {subscription.Status}");

With the existing subscription cancelled, you can set up a new one using the same mandate.

Whenever a subscription creates a payment, you’ll receive a webhook with resource_type “subscriptions” and action “payment_created”.

POST https://example.com/webhooks HTTP/1.1
User-Agent: RPN-webhook-service/1.1
Content-Type: application/json
Webhook-Signature: 68cfef92b143649f4ce9b9e4b3d182b879be77625bcd0b3ff1f37849d01f76e6
{
  "events": [
    {
      "id": "EV123",
      "created_at": "2014-08-04T12:00:00.000Z",
      "action": "payment_created",
      "resource_type": "subscriptions",
      "links": {
        "subscription": "SB123",
        "payment": "PM123",
        "organisation": "OR123"
      }
    }
  ]
}

It’s likely that you’ll want to record this newly-created payment in your database to provide your user with visibility over payments being collected, and so you can track its status over time, taking action if it fails later.

Handling payment failures

Payments collected via Direct Debit are not confirmed immediately - when a payment is created, it must be submitted to the banking system, and RPN won’t know for a few days whether the payment has been successful. For more details on how long payments take to process, see our guides for Bacs, SEPA, Autogiro, BECS and Betalingsservice.

Payment failures are rare - less than 1% of payments fail - but building a great experience for your users means handling these failures. A payment may fail, for example, if the end customer has insufficient funds in their account.

You’ll find out about a payment failure when you receive a webhook that includes an event with the resource_type of “payments” and action of “failed”:

POST https://example.com/webhooks HTTP/1.1
User-Agent: RPN-webhook-service/1.1
Content-Type: application/json
Webhook-Signature: 86f8bb84a4de63cff4af48f22b64b20970b415b066e3d21459ea515052860514
{
  "events": [
    {
      "id": "EV456",
      "created_at": "2014-08-03T12:00:00.000Z",
      "action": "failed",
      "resource_type": "payments",
      "links": {
        "payment": "PM456",
        "organisation": "OR123"
      },
      "details": {
        "origin": "bank",
        "cause": "mandate_cancelled",
        "description": "Customer cancelled the mandate at their bank branch.",
        "scheme": "bacs",
        "reason_code": "ARUDD-1"
      }
    },
    {
      "id": "EV123",
      "created_at": "2014-08-03T12:00:00.000Z",
      "action": "confirmed",
      "resource_type": "payments",
      "links": {
        "payment": "PM123",
        "organisation": "OR123"
      },
      "details": {
        "origin": "RPN",
        "cause": "payment_confirmed",
        "description": "Payment was confirmed as collected"
      }
    }
  ]
}

You’ll need to think about how to handle failures in your product - the right thing to do will depend on your product and your users’ preferences. You might consider:

  • Updating your UI to show that a payment failed, and offering the option to try again
  • Offering an option which allows your users to have failed payments automatically retried
  • Sending the end customer an email telling them what happened and what they need to do next

The details object inside the event includes more details on why the payment failed. The cause field is especially useful, providing more context on why the payment was unsuccessful. The most common causes of payment failure you’ll want to handle include:

refer_to_payer
For UK payments using the Bacs scheme, this means the end customer had insufficient funds. For Eurozone payments using the SEPA scheme, the customer must contact their bank to find our what went wrong.
insufficient_funds
Only used for SEPA, this means that the customer did not have sufficient funds.
mandate_cancelled
The mandate was cancelled between when the payment was submitted to the banks and when it was due to be debited from the end customer's account.

You can find a full list of the cause values for payment-related webhook events, including failures in our reference documentation.

You’ll need to think about how to handle failures in your product - the right thing to do will depend on your product and your users’ preferences. You might consider:

  • Updating your UI to show that a payment failed, and offering the option to try again
  • Including a setting which allows your users to have failed payments automatically retried
  • Sending the customer an email telling them what happened and what they need to do next

You can read more about reporting the status of payments to your users in our user experience guide.

If a payment has failed due to insufficient funds, you may wish to retry it. You don’t need to create a new payment to do this, but can rather retry the existing one through a simple API call:

<?php
require 'vendor/autoload.php';

$client = new \RPNPro\Client([
    'access_token' => $currentUser->RPNAccessToken,
    'environment' => \RPNPro\Environment::SANDBOX
]);

$payment = $client->payments()->get("PM000260X9VKF4");
                              // Payment ID from above

print("Status: " . $payment->status . "<br />");
print("Retrying...<br />");

$payment = $client->payments()->retry("PM000260X9VKF4");
print("Status: " . $payment->status);
import os
import RPN_pro

client = RPN_pro.Client(
    access_token=current_user.RPN_access_token,
    environment='sandbox'
)

payment = client.payments.get("PM000260X9VKF4")
                         # Payment ID from above.

print("Amount: {}".format(payment.amount))
print("Retrying...")

payment = client.payments.retry("PM000260X9VKF4")
print("Status: {}".format(payment.status))
require 'RPN_pro'

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
    environment: :sandbox
)

payment = client.payments.get('PM000269VJAR6M')
                         # Payment ID from above.

puts "Status: #{payment.status}"
puts 'Retrying...'

payment = client.payments.retry('PM000269VJAR6M')
puts "Status: #{payment.status}"
package com.gcintegration;

import com.RPN.RPNClient;
import com.RPN.resources.Payment;

public class RetryPayment {
    public static void main(String[] args) {
        RPNClient client = RPNClient
            .newBuilder(CurrentUser.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        Payment payment = client.payments().get("PM000260X9VKF4").execute();
                                              // Payment ID from above.
        System.out.printf("Amount: %s%n", payment.getAmount());
        System.out.println("Retrying...");

        payment = client.payments().retry("PM000260X9VKF4").execute();
        System.out.printf("Status: %s%n", payment.getStatus());
    }
}
// Payment ID from above
const string paymentId = "PM000260X9VKF4";

var paymentResponse = await client.Payments.GetAsync(paymentId);
var payment = paymentResponse.Payment;

Console.WriteLine($"Amount: {payment.Amount}");
Console.WriteLine("Retrying...");

var retryResponse = await client.Payments.RetryAsync(paymentId);
var payment = retryResponse.Payment;
Console.WriteLine($"Status: {payment.Status}");

Did you find this page helpful? Yes No

Getting started

Handling customer notifications

RPN automatically sends notifications to customers whenever an event happens in our system. For example, when a payment has been created, the customer will receive an email which informs them about the amount, description and cause of the payment.

Integrators have the ability to send some of these notifications themselves, and we’ll honour their request if it is made within our stated deadline: otherwise, we will send the notification ourselves.

Here is an example of the workflow:

  • Your application creates a payment
  • The payment event schedules a payment_created notification
  • We send a webhook to your application which includes notification metadata
  • Your application tells us whether it would like to handle the notification

There are two conditions that you must satisfy to be able to handle or ignore a notification:

  • You must be approved to handle notifications of this type
  • You must be the “notification owner” for that notification/resource
Getting approved to handle notifications

To be able to handle customer notifications yourself, you will need to be granted permission. Please get in touch with the RPN onboarding team to get started. Permissions are very granular, and cover the type & scheme of the notification. For example, you might be approved to handle the payment_created notification just in the scheme sepa.

Compliant notifications must include all of the required information for that notification type, and be sent within the correct interval. More information about the required information for each type of notification can be found in our platform guides.

Becoming a notification owner

A merchant may be connected to multiple partners, or have multiple integrations, so to avoid the problem of multiple integrations all trying to handle the same notification, we track the “owner” for all customer notifications in our system.

Only the owner will have the ability to handle notifications for that resource, falling back to RPN if the notification deadline is missed.

The owner is usually defined as the creator of that resource (e.g. the partner which created the payment). For example, if your integration creates a payment, the owner will be your integration. Currently our system does not permit any kind of ownership transfer from one integration to another. Resources created via dashboard will not record any owner (and therefore cannot be handled by your integration).

Getting notified about notifications

Notification information is currently only delivered in webhooks. When your application receives a webhook, each event may include a customer_notifications payload, which contains a list of one or more notifications that were triggered by that event. (If you don’t receive this information, it’s because you are not the owner for that resource or have not been granted any permissions):

POST https://example.com/webhooks HTTP/1.1
Content-Type: application/json
{
  "events": [
    {
      "id": "EV123",
      "created_at": "2018-08-03T12:00:00.000Z",
      "action": "created",
      "resource_type": "payments",
      "customer_notifications": [
        {
          "id": "PCN123",
          "type": "payment_created",
          "deadline": "2018-08-25T12:09:06.000Z",
          "mandatory": true
        }
      ]
    }
  ]
}

Note that each notification for an event includes an id, type, deadline and mandatory flag:

  • id is used to handle the notification (see below)
  • type can be used to filter notifications that you don’t handle. For example, if your integration only handles payment_created notifications, you can safely ignore notifications which are of another type.
  • deadline is the time by which your application must respond. If we don’t hear from you before this deadline, we’ll send the notification ourselves. The deadline is typically 10 minutes from the point at which we send the webhook, but in the case of payment_created notifications the deadline is the last possible time to notify the customer before the payment collects (this mirrors RPN’ behaviour).
  • mandatory is whether the notification needs to be handled by somebody (currently always true).
Handling notifications yourself

If your application intends to handle a notification, you should let us know before you take action. If you wait until after sending the notification, there is a chance that we also sent it in the meantime (e.g. if the deadline had elapsed), which would result in the customer receiving two notifications, one from each of us.

So, we recommend that you declare your intent first, and then send the notification using whichever mechanism is most appropriate for your system:

require "RPN_pro"

client = RPNPro::Client.new(
  access_token: current_user.RPN_access_token,
  environment: :sandbox,
)

RPNPro::Webhook.parse(**args).each do |event|
  notifications = event.customer_notifications

  # We might want to handle mandate_created emails and send an email which includes
  # both the mandate and upcoming payments for it.
  mandate_notifications = notifications.select { |n| n.type == "mandate_created" }

  mandate_notifications.each do |notification|
    client.customer_notifications.handle(notification.id)
    MyNotificationSystem.enqueue_email(notification, event)
  end

  # If a payment_created email was already sent, we still need to handle the notification
  # to prevent RPN from sending it.
  payment_notifications = notifications.select { |n| n.type == "payment_created" }

  payment_notifications.each do |notification|
    if notification_sent_previously?(notification)
      client.customer_notifications.handle(notification.id)
    end
  end
end

Alternatively, if your application does not respond, or does not respond before the deadline, RPN will send the notification instead and there will be no opportunity to handle after that point.

In all cases, RPN will record the outcome for each notification for audit purposes.

Did you find this page helpful? Yes No

Getting started

Reconciling payouts

Periodically (using once every working day), we’ll pay out payments your user has collected to their nominated bank account. When this happens, a “payout” record will be created which represents the transfer of funds.

It's worth noting that we will only start sending payouts once your user has verified their account - we'll send them emails reminding them to do this, but you might want to flag this in your UI, especially when they first connect their account, and perhaps afterwards until they receive their first payout. We've made some suggestions about communicating this to your users in our "User experience" guide.

We’ll generally send a single payout each day for each currency a user collects in. However, we may split the funds into multiple payouts if there are a very large number of payments to pay out.

A payout will not necessarily only contain payments collected through your integration - it may contain others if your user is also using our API, the Dashboard or another integration.

When a payout is sent to your user, you’ll receive a webhook with resource_type “payouts” and action “paid”. In addition, for each payment that has been paid out, we’ll send webhooks with resource_type “payments” and action “paid_out”. The links[payout] attribute of that webhook will include the ID of the payout the payment appears in.

For more details on handling webhooks, see the “Staying up-to-date with webhooks” section earlier in this guide.

For the majority of integrations, that’ll be enough. But some integrations (for example, accountancy software) will want to go further in understanding exactly what makes up a payout. If you want to do more complex reconciliation (for example to build a business’s accounts), the best way to understand what’s inside a payout is to use the Payout Items API.

Using the Payout Items API

The Payout Items API lets you see the individual transactions that made up the total amount of a payout.

You can think of a payout in terms of credits and debits. When we collect a payment on your user’s behalf, we add the money collected to their RPN balance - that’s a credit. But there can also be debits against that balance - for example, RPN’ fees, app fees that you charge for using your integration or deductions for chargebacks.

When we make a payout, we bundle up all of the outstanding credits and debits into it and reset the user’s balance to zero.

We'll only make a payout when a merchant's balance is positive. Imagine that a merchant's balance is zero, and they make a refund of £5.00. Their balance is now -£5.00. They then collect a payment of £10.00. Their balance is +£5.00, so we'll send them a payout, which will bundle up the refund and the successful payment into its Payout Items.

For each item in a payout, we expose an amount (which will be positive for a credit, or negative for a debit), a type (which explains the reason for the credit or debit) and links to any relevant resources (usually a payment, links.payment).

There are eight types of payout item:

Type Description Credit or debit Links
Payment paid out (payment_paid_out) A payment was successfully collected from a customer, and is being passed on to your user Credit links.payment
Payment failed (payment_failed) A payment appeared to have been collected from a customer successfully, but then the bank told us that it had in fact failed, so it is being deducted Debit links.payment
Payment charged back (payment_charged_back) A payment was successfully collected from a customer, but then the customer contacted their bank to reverse the transaction, so it is being deducted Debit links.payment
Payment refunded (payment_refunded) A payment was successfully collected from a customer, and then partially or fully refunded at your user's request, so the refunded amount is being deducted Debit links.payment
RPN fee (RPN_fee) RPN deducted its transaction fee from an incoming payment, or refunded its transaction fee when a payment failed or was charged back Credit/Debit links.payment
App fee (app_fee) Partner integrations can add an "app fee" on top of RPN's fees. RPN deducted such a fee from an incoming payment, or refunded it when a payment failed or was charged back Credit/Debit links.payment
Revenue share (revenue_share) RPN paid out a commission to a partner for a successful payment, or deducted it for a late failure or chargeback. Partner integrations can receive a share of RPN' fees when users collect payments through their integration. These do not appear in merchant payouts, since they are part of the RPN_fee, but they will appear in partner payouts when they receive the commission. Credit/Debit links.payment
Customer refunded (refund) A refund was sent to a customer, not related to any particular payment, so the refunded amount is being deducted This feature is in private beta. Debit links.mandate

To see how these can fit together in the real world, let’s imagine a payout as an example:

Description Type Amount
Payment successfully collected from Nick payment_paid_out +€20.00
RPN fee for payment collected from Nick RPN_fee -€0.20
App fee for payment collected from Nick app_fee -€1.00
Refund issued for Andrew's payment, collected last week payment_refunded -€5.00
Bianca's payment from last week, which was charged back payment_charged_back -€10.00
Refund of RPN fee for charged back payment RPN_fee +€0.10
Refund of app fee for charged back payment app_fee +€0.50
Total +€4.40

With the Payout Items API, you can look inside a payout and see the transaction that make it up, allowing you to, for example, “explain” transactions on your user’s bank statement.

When you receive a “payout paid” webhook (resource_type “payouts” and action “paid”), you can then query the Payout Items API:

<?php
require 'vendor/autoload.php';

$user = User::where(['RPN_organisation_id' => $event->links['organisation']])->firstOrFail();

$client = new \RPNPro\Client([
        'access_token' => $user->RPN_access_token,
        'environment' => \RPNPro\Environment::SANDBOX
]);

$payout_id = $event->links['payout'];

$payout = $client->payouts()->get($payout_id);
echo $payout->amount;

$payout_items = $client
        ->payoutItems()
        ->all(["params" => ["payout" => $payout_id]]);

foreach ($payout_items as $payout_item) {
        echo $payout_item->amount;
        echo $payout_item->type;
        echo $payout_item->links->payment;
}
import os
import RPN_pro

from myinvoicingapp.models import User

#...
    def process_payout_event(self, event):
        user = User.object.get(RPN_organisation_id=event['links']['organisation'])

        client = RPN_pro.Client(
            access_token=user.RPN_access_token,
            environment='sandbox'
        )

        payout_id = event['links']['payout']

        payout = client.payouts.get(payout_id)
        print payout.amount

        payout_items = client.payout_items.all(
            params={ "payout": payout_id }
        ).records

        for payout_item in payout_items:
            print payout_item.amount
            print payout_item.type
            print payout_item.links.payment
require 'RPN_pro'

def process_payment_paid_out(event)
  user = User.find_by(RPN_organisation_id: event.links.organisation)

  client = RPNPro::Client.new(
    access_token: user.RPN_access_token,
    environment: :sandbox
  )

  payout_id = event.links.payout

  payout = client.payouts.find(payout_id)
  puts payout.amount

  client.payout_items.all(params: { payout: payout_id }).each do |payout_item|
    puts payout_item.amount
    puts payout_item.type
    puts payout_item.links.payment
  end
end
package com.myInvoicingApp;

import com.RPN.resources.Event;
import com.RPN.resources.Payout;
import com.RPN.resources.PayoutItem;
import com.RPN.RPNClient;

import com.myInvoicingApp.User;

public class PayoutsWebhookHandler {
    public static void handlePayoutPaidEvent(Event event) {
        User user = User.getUserByRPNID(event.getLinks().getOrganisation());

        RPNClient client = RPNClient
            .newBuilder(user.RPNAccessToken)
            .withEnvironment(RPNClient.Environment.SANDBOX)
            .build();

        String payoutId = event.getLinks().getPayout();
        Payout payout = client.payouts().get(payoutId).execute();

        for (PayoutItem payoutItem : client.payoutItems().all().withPayout(payoutId).execute()) {
            System.out.println(payoutItem.getAmount());
            System.out.println(payoutItem.getType());
            System.out.println(payoutItem.getLinks().getPayment());
        }

    }
}
public void HandlePayoutPaidEvent(RPN.Resources.Event eventResource)
{
    var user = db.Users.Single(user => user.RPNID == eventResource.Links.Organisation);

    var client = RPNClient.Create(
        user.RPNAccessToken,
        RPNClient.Environment.SANDBOX
    );

    var payoutId = eventResource.Links.Payout;
    var payoutResponse = await client.Payouts.GetAsync(payoutId);
    var payout = paymentResponse.Payout;
    Console.WriteLine(payout.Id);

    var payoutItemsRequest = new RPN.Services.PayoutItemListRequest()
    {
        Payout = payoutId
    };

    var payoutItems = client.PayoutItems.All(payoutItemsRequest);

    foreach (RPN.Resources.PayoutItem payoutItem in payoutItems)
    {
        Console.WriteLine(payoutItem.Amount);
        Console.WriteLine(payoutItem.Type);
        Console.WriteLine(payoutItem.Links.Payment);
    }
}

For full details on the Payout Items API, head over to our reference documentation.

Did you find this page helpful? Yes No

Getting started

Importing mandates from another provider

So far, we have discussed ways to help your users set up new recurring payments, add information about new customers and so on.

However, your users may also have existing direct debit mandates set up and wish to manage them together with their RPN mandates. The mandate imports API exists to allow partners to automate this process seamlessly.

Getting set up to import mandates

If you wish to use this feature, you must first have it enabled on your partner app. To request this, get in touch with your account manager or with our partnerships team and we will respond to your request as soon as possible.

Once the feature is enabled on your account, you can start creating mandate imports. The procedure for using this feature is as follows in outline:

Creating a mandate import

The first step is simple. The only data needed for creating a mandate import is the direct debit scheme the mandates are using. Please note that a mandate import can only include mandates from one direct debit scheme, the one specified here. If you want to transfer mandates in several schemes, you will need one mandate import per scheme.

$client = new \RPNPro\Client(array(
  'access_token' => 'your_access_token_here',
  'environment'  => \RPNPro\Environment::SANDBOX
));

$mandateImport = $client->mandateImports()->create([
  "params" => ["scheme" => "sepa_core"]
]);
import RPN_pro
client = RPN_pro.Client(access_token="your_access_token_here", environment='sandbox')

mandate_import = client.mandate_imports.create(params={
  "scheme": "sepa_core"
});
@client = RPNPro::Client.new(
  access_token: "your_access_token",
  environment: :sandbox
)

mandate_import = @client.mandate_imports.create(params: {
  scheme: "sepa_core"
})

import static com.RPN.RPNClient.Environment.SANDBOX;
import static com.RPN.services.MandateImportService.MandateImportCreateRequest.Scheme.SEPA_CORE;

String accessToken = "your_access_token_here";

RPNClient client = RPNClient
    .newBuilder(accessToken)
    .withEnvironment(SANDBOX)
    .build();


MandateImport mandateImport = client.mandateImports().create()
  .withScheme(SEPA_CORE)
  .execute();
String accessToken = "your_access_token";
RPNClient RPN = RPNClient.Create(accessToken, Environment.SANDBOX);

var importRequest = new RPN.Services.MandateImportCreateRequest()
{
    Scheme = MandateImportCreateRequest.MandateImportScheme.SepaCore
};

var importResponse = await RPN.MandateImports.CreateAsync(importRequest);
RPN.Resources.MandateImport mandateImport = importResponse.MandateImport;
POST https://api.pymnt.us/mandate_imports HTTP/1.1
{
  "mandate_imports": {
    "scheme": "sepa_core"
  }
}

HTTP/1.1 201 (Created)
Location: /mandate_imports/IM000010790WX1
{
  "mandate_imports": {
    "id": "IM000010790WX1",
    "scheme": "sepa_core",
    "status": "created",
    "created_at": "2018-03-12T14:03:04.000Z"
  }
}
Adding mandate import entries

Once we have created the mandate import itself, the next step is to add mandate import entries for each mandate we want to import. To create a mandate import entry, we must provide:

  • the ID of the mandate import we are populating
  • a customer sub-resource, containing identifying data about the customer to be charged by the mandate (required fields differ between schemes; see the API reference for details)
  • a bank_account sub-resource, containing the account holder name bank details (either an IBAN or local details)

For mandates in the SEPA scheme, you must also provide amendment details, which identify the current state of the mandate. See the code examples below and the API reference for details.

It is also possible - and recommended - to provide a record_identifier value, a string which should be unique per mandate import. This will make it easier to reconcile the imports with records in your own system later.

$mandateImportEntry = $client->mandateImportEntries()->create([
  "params" => [
    "links" => [
      "mandate_import" => $mandateImport->id
    ],
    "record_identifier" => "bank-file.xml/line-1",
    "customer" => [
      "company_name" => "Théâtre du Palais-Royal",
      "email" => "moliere@tdpr.fr"
    ],
    "bank_account" => [
      "account_holder_name" => "Jean-Baptiste Poquelin",
      "iban" => "FR14BARC20000055779911"
    ],
    // Amendment details are required for SEPA only
    "amendment" => [
      "original_mandate_reference" => "REFNMANDATE",
      "original_creditor_id" => "FR123OTHERBANK",
      "original_creditor_name" => "Amphitryon"
    ]
  ]
]);
mandate_import_entry = client.mandate_import_entries.create(params={
  "links": {
    "mandate_import": mandate_import.id
  },
  "record_identifier": "bank-file.xml/line-1",
  "customer": {
    "company_name": "Théâtre du Palais-Royal",
    "email": "moliere@tdpr.fr"
  },
  "bank_account": {
    "account_holder_name": "Jean-Baptiste Poquelin",
    "iban": "FR14BARC20000055779911"
  },
  # Amendment details are required for SEPA only
  "amendment": {
    "original_mandate_reference": "REFNMANDATE",
    "original_creditor_id": "FR123OTHERBANK",
    "original_creditor_name": "Amphitryon"
  }
})
mandate_import_entry = @client.mandate_import_entries.create(params: {
  links: {
    mandate_import: mandate_import.id
  },
  record_identifier: "bank-file.xml/line-1",
  customer: {
    company_name: "Théâtre du Palais-Royal",
    email: "moliere@tdpr.fr"
  },
  bank_account: {
    account_holder_name: "Jean-Baptiste Poquelin",
    iban: "FR14BARC20000055779911"
  },
  # Amendment details are required for SEPA only
  amendment: {
    original_mandate_reference: "REFNMANDATE",
    original_creditor_id: "FR123OTHERBANK",
    original_creditor_name: "Amphitryon"
  }
})

MandateImportEntry mandateImportEntry = client.mandateImportEntries().create()
  .withCustomerCompanyName("Théâtre du Palais-Royal")
  .withCustomerEmail("moliere@tdpr.fr")
  .withBankAccountAccountHolderName("Jean-Baptiste Poquelin")
  .withBankAccountIban("FR14BARC20000055779911")
  // Amendment details are required for SEPA only
  .withAmendmentOriginalMandateReference("REFNMANDATE")
  .withAmendmentOriginalCreditorId("FR123OTHERBANK")
  .withAmendmentOriginalCreditorName("Amphitryon")
  .withLinksMandateImport(mandateImport.getId())
  .execute();
var request = new RPN.Services.MandateImportEntryCreateRequest()
{
  Customer = new RPN.Services.MandateImportEntryCreateRequest.MandateImportEntryCustomer()
  {
    CompanyName = "Théâtre du Palais-Royal"
    Email = "moliere@tdpr.fr"
  }
  BankAccount = new RPN.Services.MandateImportEntryCreateRequest.MandateImportEntryBankAccount()
  {
    AccountHolderName = "Jean-Baptiste Poquelin"
    Iban = "FR14BARC20000055779911"
  }
  // Amendment details are required for SEPA only
  Amendment = new RPN.Services.MandateImportEntryCreateRequest.MandateImportEntryAmendment()
  {
    OriginalMandateReference = "REFNMANDATE"
    OriginalCreditorId = "FR123OTHERBANK"
    OriginalCreditorName = "Amphitryon"
  }
  Links = new RPN.Services.MandateImportEntryCreateRequest.MandateImportEntryLinks()
  {
    MandateImport = mandateImport.Id
  }
};

var importResponse = await RPN.MandateImportEntries.CreateAsync(request);
RPN.Resources.MandateImportEntry entry = importResponse.MandateImportEntry;

POST https://api.pymnt.us/mandate_import_entries HTTP/1.1
{
  "mandate_import_entries": {
    "links": {
      "mandate_import": "IM000010790WX1"
    },
    "record_identifier": "bank-file.xml/line-1",
    "customer": {
      "company_name": "Théâtre du Palais-Royal",
      "email": "moliere@tdpr.fr"
    },
    "bank_account": {
      "account_holder_name": "Jean-Baptiste Poquelin",
      "iban": "FR14BARC20000055779911"
    },
    "amendment": {
      "original_mandate_reference": "REFNMANDATE",
      "original_creditor_id": "FR123OTHERBANK",
      "original_creditor_name": "Amphitryon"
    }
  }
}

HTTP/1.1 201 (Created)
{
  "mandate_import_entries": {
    "record_identifier": "bank-file.xml/line-1",
    "created_at": "2018-03-03T00:00:00Z",
    "links": {
      "mandate_import": "IM000010790WX1"
    }
  }
}

If you provide invalid data, a descriptive error will be returned so you can make corrections.

Submitting the mandate import for review

When all entries have been added to the mandate import, it is time to submit it for review.

In order to defend against possible fraudulent or harmful use of this feature, all submitted imports are subject to review by RPN staff. We aim to complete this as quickly as possible.

$client->mandateImports()->submit($mandateImport->id);
client.mandate_imports.submit(mandate_import.id)
@client.mandate_imports.submit(mandate_import.id)
client.mandateImports().submit(mandateImport.getId()).execute();
await RPN.MandateImports.SubmitAsync(mandateImport.Id);
POST https://api.pymnt.us/mandate_imports/IM000010790WX1/actions/submit HTTP/1.1

HTTP/1.1 200 (OK)
{
  "mandate_imports": {
    "id": "IM000010790WX1",
    "scheme": "bacs",
    "status": "submitted",
    "created_at": "2018-03-12T14:03:04.000Z"
  }
}
Linking up resources

After approval, the mandate import will be processed. As mandates are migrated, you will receive webhooks, just as if they had been created using the mandates API.

If you need to reconcile the new resources with your system, periodically check your mandate import. When its status is processed, listing the mandate import entries will give you the identifiers for the customer, bank account and mandate resources that were created for each mandate import entry.

$mandateImport = $client->mandateImports()->get(mandateImport->id);

if ($mandateImport->status === "processed") {
    doReconciliation();
}

mandate_import = client.mandate_imports.get(mandate_import.id)

if mandate_import.status == "processed":
    do_reconciliation()
mandate_import = @client.mandate_imports.get(mandate_import.id)

if mandate_import.status == "processed"
  do_reconciliation
end
MandateImport mandateImport = client
    .mandateImports()
    .get(mandateImport.getId())
    .execute();

if (mandateImport.getStatus() == "processed") {
    doReconciliation();
}
var response = await RPN.MandateImports.GetAsync(mandateImport.Id);
RPN.Resources.MandateImport import = response.MandateImport;

if (import.Status == MandateImportStatus.Processed)
{
    DoReconciliation();
}
GET https://api.pymnt.us/mandate_imports/IM000010790WX1 HTTP/1.1

HTTP/1.1 200 (OK)
{
  "mandate_imports": {
    "id": "IM000010790WX1",
    "scheme": "bacs",
    "status": "created",
    "created_at": "2018-03-12T14:03:04.000Z"
  }
}
$entries = $client->mandateImportEntries()->all([
  "params" => ["mandate_import" => "IM000010790WX1"]
]);

foreach ($entries as $i => $entry) {
  echo $entry->recordIdentifier;
  echo $entry->links['customer'];
  echo $entry->links['customer_bank_account'];
  echo $entry->links['mandate'];
}
response = client.mandate_import_entries.all(
  params={ "mandate_import": mandate_import.id }
).records

for entry in records:
    print(entry.record_identifier)
    print(entry.links.customer)
    print(entry.links.customer_bank_account)
    print(entry.links.mandate)
@client.mandate_import_entries.all(
  params: {
    "mandate_import" => mandate_import.id
  }
).each do |entry|
  puts entry.record_identifier
  puts entry.links.customer
  puts entry.links.customer_bank_account
  puts entry.links.mandate
end

for (MandateImportEntry entry : client.mandateImportEntries().all().withMandateImport("IM000010790WX1").execute()) {
  System.out.println(entry.getRecordIdentifier());
  System.out.println(entry.getLinks().getCustomer());
  System.out.println(entry.getLinks().getCustomerBankAccount());
  System.out.println(entry.getLinks().getMandate());
}
var request = new RPN.Services.MandateImportEntryListRequest()
{
    MandateImport = mandateImport.Id
};

var response = RPN.MandateImportEntries.All(request);
foreach (RPN.Resources.MandateImportEntry entry in response)
{
    Console.WriteLine(entry.RecordIdentifier);
    Console.WriteLine(entry.Links.Customer);
    Console.WriteLine(entry.Links.CustomerBankAccount);
    Console.WriteLine(entry.Links.Mandate);
}
GET https://api.pymnt.us/mandate_import_entries?mandate_import=IM000010790WX1 HTTP/1.1

HTTP/1.1 200 (OK)
{
  "mandate_import_entries": [
    {
      "record_identifier": "bank-file.xml/line-2",
      "created_at": "2018-03-03T00:00:01Z",
      "links": {
        "mandate_import": "IM000010790WX1"
      }
    },
    {
      "record_identifier": "bank-file.xml/line-1",
      "created_at": "2018-03-03T00:00:00Z",
      "links": {
        "mandate_import": "IM000010790WX1"
      }
    }
  ],
  "meta": {
    "cursors": {
      "before": null,
      "after": null
    },
    "limit": 50
  }
}

If, having created a mandate import, you need to cancel it for any reason, use the provided cancellation API. Please note that once a submitted mandate import has been approved by our team, it can no longer be cancelled or reversed.

Did you find this page helpful? Yes No

Getting started

User experience

This section of our guide is intended to help you build the best experience for your users, ensuring that they’re able to easily enable Direct Debit payments using RPN and that they’re able to effectively manage their payments — both when everything works as expected and also inevitably when sometimes it doesn’t (for example when payments fail, or mandates are cancelled).

For this section, we’ve imagined a simple invoicing application (called ‘Invoicer’) to help illustrate the points we’d like to draw your attention to. It’s difficult here to cover absolutely everything you’ll encounter when applying the principles below to your integration, as you’ll no doubt experience your own unique challenges based on your use case — but the sections of this guide should serve as a checklist for things you should consider when integrating with RPN.

In this guide, we’ll consider the following areas:

  • How to let merchants know they can use your RPN integration within your product
  • Providing options for merchants to configure your integration
  • Best practices for managing & reporting payments, mandates
  • The end customer experience
Bringing the integration to your users’ attention

The first thing to bear in mind is how you help your users understand that Direct Debit payments are something that is available from your application. Here are some ideas for how you can achieve this:

  • Promote your integration from the home page of the product, avoiding simply hiding it in a ‘Settings’ section:
  • Promote your integration at other key points of your user’s journey around your product, such as when they’re viewing customers or invoices, or when creating an invoice:
  • Describe the benefits of the integration — in our example, clicking ‘Start taking Direct Debit payments’ from the above screens will take them to this section, giving more detail about what the user will gain from enabling the integration:

It would also be good to consider adding other resources to this page such as an FAQ or a video (as we’ve suggested in the screenshot above) to help merchants understand as much as possible before enabling Direct Debit payments.

Once your users have enabled RPN

After your users have completed the setup process with RPN and have returned to your app, consider the following:

  • It’s likely that your user will need to go through RPN’s onboarding flow, providing further information about themselves to get their account verified - you can check whether this is required through the API, and direct the user to our onboarding flow if needed
  • You could now offer ways for them to configure their integration, for example turning on automatic invoice reconciliation, or choosing whether to automatically retry failed payments
  • At this point you may also want to offer some tips on how to get started with their RPN integration
Getting approval to take Direct Debit payments from your customers

In our example application, we’re going to focus on enabling Direct Debit payments by sending out mandate requests to our customers via email. If you’re also sending out mandate emails via your product, make sure that your users can configure the email’s contents:

Make it easy for your users to send their end customers mandate requests — in the following examples, we’ve created an ‘Actions’ dropdown for each individual customer (when viewing all customers together), and we’ve also included a ‘Setup Direct Debit mandate’ button when viewing an individual customer:

You should also consider allowing your users to perform this action in bulk:

You can also provide a mandate setup link in your product that your merchants can copy and use wherever they like, for example in emails or on their website:

From the end customer’s perspective, you could also enable ‘Pay with Direct Debit’ buttons on electronic invoices sent by your users to any of their customers who do not yet have Direct Debit enabled:

You can also make it as easy as possible to send Direct Debit requests to customers at other points in your user’s journey — in our example, we’ve imagined adding options for this when our users are creating customers, or when creating invoices for customers without Direct Debit enabled:

In the example ‘Create new invoice’ screen above, we’ve included a ‘Recurring’ dropdown. In our imagined application, you can create regular invoices that would be issued to customers at a frequency of the merchant’s choosing — in this case, we would also take advantage of being able to use Direct Debit to take recurring payments for those regular invoices.

Clearly displaying the status of mandates to your users

It’s important to help your users understand exactly what state each of their Direct Debit mandates with their customers is in — in our example application, as you’ll see below, we’re going to show the mandate status of each customer when viewing all customers and also when viewing individual customers:

You should highlight mandates that have been cancelled & encourage your user to contact their customer in this case:

Consider also providing a way for users to re-send mandate requests to customers, in the event that there is a significant delay in any response from the customers:

You can also let your users know about the ability to import existing mandates they may have already set up using the RPN dashboard or via another integration (if you’ve included support for importing existing mandates in your integration).

Reporting on the status of payments

It’s important to help your users understand the status of every payment they’re taking. In our example, we’ll use our ‘Invoices’ section to display the status of the payment of each invoice:

In the event that there are problems with any payment, you should alert your users and help them understand the best thing for them to do to act on these problems — for example:

  • You can highlight failed payments, clearly display the reason for the failure, ensure the invoices are not marked as paid and make it as easy as possible for your users to retry these payments
  • In our example, we’ve also given the option to resend a Direct Debit mandate request to customers whose payments have failed as a result of issues with a mandate

At this point, you could also remind users about automatically retrying failed payments (if you’re supporting this feature) if they haven’t enabled this option already.

Scheduling payments in advance

Consider giving your users the option to schedule payments in advance — for example, when creating an invoice, in our example application we’ve given the option to either take the payment as soon as possible, or specify a date in the future when this payment should be taken:

In our example above, we’ve given the option to schedule a date in the future when the payment should be charged, which means that we would have to ensure that the technical solution creates the payment enough in advance of this date to ensure it’s charged on the date required by the merchant.

Mandate setup confirmation for customers

Consider designing a confirmation screen for your users’ customers to be redirected to once they’ve completed their mandate setup, to

Once a customer has finished setting up their Direct Debit, show them a clear confirmation page telling them that their Direct Debit has been set up, and what will happen next. You can either build your own or use our pre-made one - see the end of our “Setting up Direct Debit mandates” guide for details.

Did you find this page helpful? Yes No

Getting started

What’s next?

Having followed our guides and built & tested your integration, check out our best practices checklist - it’ll take you through everything you need to build a great integration for your users, and is perfect for going through before you go live.

Going live

Once you’ve fully tested your partner integration in the sandbox, you’ll need to have it reviewed by a member of the RPN team before you can start connecting merchants in the live environment. To start this process, you’ll need to complete a self-assessment, which should take around 30 minutes.

Make sure you’ve set up your account, created your live app, and then click the “Begin self-assessment” button on your app’s page.

Once you’ve completed the self-assessment, we’ll aim to get back to you within 3 working days. We’ll let you know when your app has been reviewed, but as part of the review process, we may send you feedback, or ask for a demo of your application - if so, we’ll be in touch.

You’ll also need to write a user guide so it’s clear how your integration works. We’ll need a high resolution logo, so we can list you as one of our partners. Please send a URL for your user guide, and the logo, to partnerships@pymnt.us or your account manager.

Finally, to drive usage of your integration, we’d suggest developing a landing page with the key benefits and a user guide, ensuring RPN is included in your FAQs and support materials, pointing users to it in welcome emails and regular communications.

Earning money from your integration

RPN offer a 10% revenue share of the transaction fees generated by your integration.

That means that for every transaction that your users take through your integration, you’ll receive a 10% share of the fees RPN collect.

Please note that if you have chosen to apply app fees to your integration, you cannot also have a revenue share.

For more information or to set up a revenue share, contact the partnerships team.

Did you find this page helpful? Yes No

Getting started

Best practices

We want to help you to build a great integration that will provide a great experience and deliver value to your users. Following through this “best practices” checklist is a great way of making sure you’ve done everything you need to do to go live successfully.

Account set up

Have you given your app a sensible, public-facing name?

  • Your users will see this when they give you access to their RPN accounts through the OAuth flow, and it’ll also appear on our reporting.

Have you verified your account for payouts?

  • This is important if you are receiving a revenue share or adding app fees, as we cannot pay those out otherwise.

Have you added contact details to your account?

  • Your contact details are displayed to your clients, so make sure they’re up to date.

Are you based in the same country as your merchants will be?

  • If you are in the UK then we’ll enable Bacs by default for you. If you are in the Eurozone then your account will be enabled for SEPA by default.
  • You may need to contact our API support team to enable the right scheme on your account.
Merchant onboarding

Are you using OAuth to get access to your users’ accounts?

  • OAuth allows a merchant to easily and securely give you access to their account, so you can use the RPN API on their behalf (more details).

Are you passing the initial_view parameter when you create an OAuth link?

  • When creating an authorisation link, you can specify whether the OAuth flow should default to showing a login screen or a signup screen allowing your user to create a RPN account (more details).

Are you prefilling merchants’ email addresses?

  • If you already know your merchants email address, make sure to prefill the OAuth flow accordingly. This can be done by specifying the prefill[email] parameter. You can also prefill their given and family names, and their organisation name (e.g. Acme Widget plc). (more details)

Have you made it clear that merchants will need to verify their account before they can receive payouts?

  • Before a merchant can receive payouts, they need to go through RPN’s onboarding and verification process and provide more details about themselves
  • Using the API, you can check your users’ verification state and send them to the onboarding flow if required (more details)
  • RPN will also send your users email notifications reminding them to verify their account

Have you made it easy to configure the merchant’s account to work well with RPN once they’ve connected? For example, can they configure:

  • when payments should be collected?
  • how mandates are set up?
  • any reconciliation settings?

If you are charging app fees, is it clear to merchants that you are charging app fees, and if so how much?

Mandate set up

Is it easy for merchants to request customers to set up mandates? For example:

  • Can merchants send requests to multiple customers in batch?

Can the merchant clearly see which customers have set up a mandate?

Can the merchant send reminders to customers to set up mandates?

  • A customer might miss the notification to create a mandate. Therefore it’s good practice to allow sending a reminder after some time has passed.

What happens if the mandate fails to be set up? For example:

  • Do you inform your user? Do you re-open the invoice?
  • Do you help your user to understand what has gone wrong? (more details)

What happens if the mandate is cancelled? For example:

  • Do you tell your user?
  • Do you provide a way to reinstate the mandate?
  • Do you help your user to understand what has gone wrong? (more details)

What happens if the mandate expires? For example:

  • Do you tell your user?
  • Can you reinstate the mandate?

Can an existing RPN merchant import their mandates and match them to customers?

Can you manage bulk changes (transfers of existing Direct Debits from another provider)?

If you offer multiple payment options to your users’ customers, how do you refer to your RPN integration?

  • Make sure to refer to your RPN integration as ‘pay by Direct Debit’ (or corresponding local scheme names) as this is the underlying payment method.
  • Avoid terms like ‘pay by bank transfer’, ‘pay by RPN’ or ‘bank payments’ as these might confuse end customers.
RPN hosted payment pages

When using the payment pages hosted by RPN, have a look at the following items:

Is it intuitive for end customers to set up mandates? For example:

  • Do you include instructions or payment links in emails?
  • Do you link to the payment pages from invoices? How does this work for future payments?.
  • Do you link to the payment pages from your website?

Are you using the description parameter to show a description of what the end customer is signing up for?

Does your redirect URL (after setting up a mandate) look great for an end customer? For example:

  • Does it make clear that Direct Debit is set up?
  • Does it describe what happens next? For example, does it mention when the customer will be charged?
Custom payment pages

Merchants on the Pro package can have custom payment pages. Have a look at the following points to give them the best experience:

If you or your merchants use custom payment pages:

  • Have your merchant’s payment pages been reviewed by RPN?
  • Do you make it easy for merchants to create end customers with RPN by handling that for them? (e.g. a merchant collects bank details on their custom payment pages, and directly forwards them to you. Your integration uses those details to create customers and mandates via RPN.)

If you support setting up mandates by phone:

  • Has the script been reviewed and will calls be recorded?
  • Are you doing modulus checking using the Bank Details Lookups API? (more details)

If your merchant is passing you bank details, are you doing modulus checking?

  • Modulus checking is used to do basic validity checking of bank details.
  • Are you doing modulus checking using the Bank Details Lookups API? (more details)
Transferring bank accounts

Are you aware that we only expose the last 2 digits from bank account when a customer switches bank account?

  • For security reasons, we only expose the last 2 digits of bank account information via API.
  • We’ll send a “transferred” webhook when a customer switches bank accounts (more details)
  • You can retrieve limited details about an end customer’s bank account via the API (more details)
Payment collection

How are you creating payments? Do you create individual payment requests or subscriptions?

  • Subscriptions currently do not support app fees.
  • If you’re creating one-off payments, be aware that we will send customer notifications for every payment unless your user is on the RPN Pro package.

Can the merchant create a payment as soon as a customer has set up a mandate?

  • You don’t need to wait until the mandate is active.

Do you allow merchants to schedule payments in advance?

  • When creating payments, you can specify an optional charge_date (more details).

Do you enable merchants to collect both one-off and recurring payments?

For single one-off payments, do you store our mandate ID?

  • Storing the mandate ID allows you to connect a customer in your database with the mandate on RPN. This means you can use the same mandate for future payments and ensures that the customer doesn’t need to input their bank details again for further payments.
  • You can either use the mandate to automatically take future payments (of which the customer will be notified), or get the customer to approve the payment.

Are you aware of our maximum £5k limit per transaction?

Do you allow merchants to retry payments that fail? (more details)

  • A payment can be retried up to 3 times
  • Do you provide automation to retry payments?
  • You should only retry payments that fail due to insufficient funds.

What happens when payments fail? For example:

  • Do you notify the merchant?
  • Do you tell them why it failed?
  • Do you reopen the invoice?
  • Do you give an option to retry the payment (or automate this)?

Do you use idempotency keys to avoid duplicate payments being created? (more details)

  • You can safely retry creating payments by using idempotency keys.

Are you charging app fees? (more details)

  • Make sure that app fees is never more than 50% of any given payment amount.
Refunds

Refunds are only available for merchants on our Pro package.

Do you need refunds enabled for your users?

Are you aware that you can trigger refunds via the API for users that have them enabled? (more details)

Are you aware that we can only refund against a specific transaction?

  • This means, a refund shouldn’t be used for more general payouts like goodwill credits.

Are you making sure to only enable refunds until at least 2 days after customer is charged?

  • This avoids refunding on a payment that later fails.

Do you know that we can still refund a payment even after the mandate has been cancelled?

Payouts & reconciliation

How are you handling reconciliation when we pay out to your user? For example: (more details)

  • Do you mark the corresponding invoice as paid?
  • Are fees (included in payouts) shown and posted as expenses?
Testing

Have you tested your integration in the sandbox before going live?

  • In the sandbox, you can test your integration without real money changing hands. We provide test bank details to make it as simple as possible (more details).
  • Sandbox allows you to manually trigger certain cases (like a customer cancelling their mandate or a payment failing due to insufficient funds) so you can test how your integration responds (more details).

Have you connected a separate RPN account to your app?

  • To mirror a separate merchant connecting to your app, we recommend testing your integration with a separate account.
  • Ensure you aren’t using real email addresses in sandbox, as this will send notifications.

Have you tested payments with your live account?

  • After successfully testing in sandbox, you might want to test your integration in live to make sure it works as expected.

Have you tested the redirect URL flow?

  • RPN partner apps can have up to twenty redirect URLs each, which is useful for testing. Edit your app in the dashboard to add endpoints for your development, QA or other environments.
Error handling

How does your integration handle API errors?

  • Our API reference includes details on errors returned by the API and how to handle them.

Are you aware of our API rate-limiting?

  • This might be relevant if you have very high volume merchants. Head to our API reference for further details.
Brand guidelines

Are you using the correct RPN logo?

Promoting the integration

Is it clearly visible how to sign up to RPN? For example:

  • If you have created a payments tab, you could link to your RPN integration from there
  • Add an alert on to your homepage
  • Add relevant in-product prompts, for example when merchants create an invoice or sign a contract
  • Embed or promote setting up payments as part of the new user onboarding experience
  • Include relevant information and links in welcome emails and regular communications
  • Create a landing page with benefits
  • Document your RPN integration in the user guide and make it easy to find
  • Add information on your RPN integration in the FAQs and help section

Did you find this page helpful? Yes No

Getting started

Scenario simulators

When you’re building an integration with the API, there are some common paths you should make sure your integration handles successfully, for example a customer cancelling their mandate or a payment failing due to lack of funds.

In the sandbox environment, we provide scenario simulators which allow you to manually trigger certain cases so you can test how your integration responds. There are two types of scenario simulator: Dashboard-triggered scenarios, and name-triggered scenarios.

Dashboard-triggered scenarios

Dashboard-triggered scenarios are started from your Dashboard. Just head to the "Developers" tab on the left-hand side, and then click "Simulate a scenario".

Choose a simulator from the dropdown (for example "Payment paid out"), and then you'll see more information about that scenario, detailing exactly what will happen and anything you need to know before you can use it (for example, some scenario simulators are not compatible with every scheme).

Enter the ID of the resource you want to run the simulator on, and then click "Simulate scenario". If there's any problem, for example we can't find the resource or it isn't compatible with the scenario you've chosen, we'll let you know.

You can use Dashboard-triggered scenarios to try all of the cases supported by the name-triggered scenarios below, plus a few extras (for example creditor verification status, useful for partners).

Name-triggered scenarios

Where possible, you should use Dashboard-triggered scenarios, since they are more reliable and support a wider range of cases.

Name-triggered scenarios are scenarios started by using special customer names. For example, if you create a customer with the given_name “Successful”, their mandate will be activated immediately, and their payments will be paid out straight away.

These customer names take effect only on payments created as an individual payment, not those created through a subscription.

All the relevant events and webhooks will be created and sent for these simulators.

Customer given_name Description What happens on mandate creation What happens on payment creation Schemes supported
Successful The customer’s payment is collected successfully and paid out to you. The mandate is marked as submitted, then activated. The payment is marked as submitted, then confirmed. If you’ve set up a creditor bank account, it is then paid_out and a payout is created. Bacs, BECS, Betalingsservice, SEPA Core and Autogiro
Penniless The customer’s payment can’t be collected, for example because they don’t have enough money in their account. The mandate is marked as submitted, then activated. The payment is marked as submitted, then failed. Bacs, BECS, Betalingsservice, SEPA Core and Autogiro
Fickle The customer’s payment is collected successfully, but is then charged back by the customer disputing it with their bank. The mandate is marked as submitted, then activated. The payment is marked as submitted, then confirmed. If you’ve set up a creditor bank account, it is then paid_out and a payout is created. Finally, the payment is marked as charged_back. Bacs, BECS, Betalingsservice, SEPA Core and Autogiro
Late The customer’s payment can’t be collected, but the bank informs us of the failure later than normal. The mandate is marked as submitted, then activated. The payment is marked as submitted, then confirmed, then failed. Bacs, BECS, and SEPA Core
Invalid The customer’s mandate can’t be set up because their bank details are rejected by the banks as invalid. The mandate is marked as submitted, then failed. N/A Bacs, BECS, Betalingsservice and Autogiro
Expired The customer's Bacs or SEPA mandate has expired because no collection attempts were made against it for longer than the scheme's dormancy period (13 months for Bacs, 15 months for BECS and Betalingsservice, 3 years for SEPA). The mandate is marked as submitted, then activated, then expired. N/A Bacs, BECS, Betalingsservice and SEPA Core
Switching The customer has an existing Bacs mandate which is transferred to another bank account using the UK’s Current Account Switching Service. The mandate is marked as submitted, then active, then transferred. N/A Bacs only

For SEPA mandates, “what happens on mandate creation” will only occur when the first payment is taken.

Did you find this page helpful? Yes No

Getting started

Test webhooks

Adding support for webhooks allows you to receive real-time notifications from RPN when things happen in your account, so you can take automated actions in response.

When you’re building your integration, you’ll want to receive realistic webhooks so you can test how your integration responds.

You can trigger webhooks using our scenario simulators, or with the “Send test webhook” tool - just click “Send test webhook” on your “Developers” page in the dashboard.

Before you can use the “Send test webhook” tool, you’ll need to have a webhook endpoint set up. Click “Create" then “Webhook endpoints” on the “Developers” page in your dashboard. If you’ve created any apps, their webhook endpoints will also be available.

You’ll then be able to customise your webhook. First, choose your webhook endpoints, and then set the resource type, action, cause, event details and associated ID.

In your code, you’re likely to use the ID to locate records in your database and take relevant actions based on your webhook.

Click “Send test webhook”, and we’ll send the webhook to your chosen endpoint, usually within a few seconds.

Refresh the page, and the webhook you’ve just sent will appear in your list of webhooks.

Click on a webhook, and you can see the full request, plus your response. This is really useful for debugging purposes.

You can resend a webhook you’ve already sent by choosing it from the list, then clicking “Retry” in the top-right hand corner.

Did you find this page helpful? Yes No

Getting started

Test bank details

In the sandbox, you can set up payments using our test details - this means, for example, that you can test Swedish payments without having your own Swedish bank account:

  • For Bacs, use the sort code 200000 and the account number 55779911
  • For SEPA, use the French IBAN FR1420041010050500013M02606
  • For Autogiro,use the clearingnummer (branch code) 5491, the kontonummer (account number) 0000003 and the personnummer (Swedish identity number) 198112289874
  • For Betalingsservice, use the registreringsnummer (bank code) 345, the kontonummer (account number) 3179681 and the CPR-nummer (Danish identity number) 0101701234
  • For BECS, use the BSB 082-082 and the account number 012345678

You can find more country-specific bank details in our appendix.

Did you find this page helpful? Yes No

Getting started

Events

Webhooks are backed by events. We record an event whenever something happens in your account, for example when a mandate is created, a payment fails to be collected due to insufficient funds or a subscription is cancelled.

We’ll record an event whenever something happens in your account, whether it’s triggered through the API or the dashboard.

You can browse your events from your Dashboard, providing a helpful audit trail for your account.

On your dashboard, just click the “Events” button on the left-hand side (it looks like a heartbeat).

You’ll see a list of your events, which you can filter by resource, action and date. Click on an event in the list to go to the resource it pertains to.

Did you find this page helpful? Yes No