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:
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.
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.
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.
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:
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.
Generate the payment short code for your invoice
Text message the payment link to your mobile phone number
Make a payment using your mobile wallet or card information
Once you receive a confirmation of the payment, your integration is complete.
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:
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
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.
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.
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.
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:
There are two conditions that you must satisfy to be able to handle or ignore a notification:
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.
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).
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:
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.
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:
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.
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.
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:
The process for your user looks like this:
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.
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:
https://connect-sandbox.pymnt.us/oauth/authorize
. It'll form the
base of the URL your client library generates.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
read_write
or read_only
.initial_view
(optional)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)prefill[email]
(optional)prefill[given_name]
(optional)prefill[family_name]
(optional)prefill[organisation_name]
(optional)language
(optional)<?php
require 'vendor/autoload.php';
// You should store your client ID and secret in environment variables rather than
// committing them with your code
$client = new OAuth2\Client(getenv('RPN_CLIENT_ID'), getenv('RPN_CLIENT_SECRET'));
$authorizeUrl = $client->getAuthenticationUrl(
// 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.
'https://connect-sandbox.pymnt.us/oauth/authorize',
'https://acme.enterprises/redirect', // Your redirect URL
['scope' => 'read_write',
'initial_view' => 'login',
'prefill' => ['email' => 'tim@pymnt.us',
'given_name' => 'Tim',
'family_name' => 'Rogers',
'organisation_name' => 'Tim\'s Fishing Store']]
);
// You'll now want to direct your user to the URL - you could redirect them or display it
// as a link on the page
header("Location: " . $authorizeUrl);
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",
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",
prefill={
"email": "tim@pymnt.us",
"given_name": "Tim",
"family_name": "Rogers",
"organisation_name": "Tim's Fishing Store"
}
)
authorize_url = flow.step1_get_authorize_url()
# You'll now want to direct your user to the URL - you could redirect them or display it
# as a link on the page
flask.redirect(authorize_url, code=302)
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')
authorize_url = oauth.auth_code.authorize_url(redirect_uri: 'https://acme.enterprises/redirect',
scope: 'read_only',
initial_view: 'signup',
prefill: {
email: "tim@pymnt.us",
given_name: "Tim",
family_name: "Rogers",
organisation_name: "Tim's Fishing Store"
})
redirect_to authorize_url
// See https://raw.githubusercontent.com/RPN/RPN-pro-java-maven-example/master/src/main/java/hello/OAuthController.java
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
public class OAuthController {
private static final String authoriseRedirectUrl =
"https://connect-sandbox.pymnt.us/oauth/authorize";
private static final String redirectUrl =
"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");
@GetMapping("/")
public ResponseEntity<String> index() {
String oauthUrl = UriComponentsBuilder
.fromUriString(authoriseRedirectUrl)
.queryParam("client_id", clientID)
.queryParam("redirect_uri", redirectUrl)
.queryParam("scope", "read_write")
.queryParam("response_type", "code")
.queryParam("initial_view", "signup")
.queryParam("prefill[email]", "tim@pymnt.us")
.queryParam("prefill[given_name]", "Tim")
.queryParam("prefill[family_name]", "Rogers")
.queryParam("prefill[organisation_name]", "Tim's Fishing Store")
.toUriString();
HttpHeaders headers = new HttpHeaders();
headers.add("Location", oauthUrl);
return new ResponseEntity<String>(oauthUrl, headers, HttpStatus.FOUND);
}
}
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.
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.
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:
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.
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
);
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:
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).
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.
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.
You have two options for setting up Direct Debit mandates:
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.
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:
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:
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:
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.
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.
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:
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.
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
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.
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:
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);
}
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.
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.
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:
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.
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:
Read more about reporting the status of mandates to your users in our user experience guide.
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.
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:
You can support importing mandates in two main ways:
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.
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:
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.
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.
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:
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.
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.
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}");
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.
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:
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:
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:
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}");
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:
There are two conditions that you must satisfy to be able to handle or ignore a notification:
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.
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).
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:
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.
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.
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.
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.
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:
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"
}
}
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:
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.
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"
}
}
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.
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:
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:
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.
After your users have completed the setup process with RPN and have returned to your app, consider the following:
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.
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).
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:
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.
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.
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.
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.
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.
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.
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.
Have you given your app a sensible, public-facing name?
Have you verified your account for payouts?
Have you added contact details to your account?
Are you based in the same country as your merchants will be?
Are you using OAuth to get access to your users’ accounts?
Are you passing the initial_view parameter when you create an OAuth link?
Are you prefilling merchants’ email addresses?
Have you made it clear that merchants will need to verify their account before they can receive payouts?
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:
If you are charging app fees, is it clear to merchants that you are charging app fees, and if so how much?
Is it easy for merchants to request customers to set up mandates? For example:
Can the merchant clearly see which customers have set up a mandate?
Can the merchant send reminders to customers to set up mandates?
What happens if the mandate fails to be set up? For example:
What happens if the mandate is cancelled? For example:
What happens if the mandate expires? For example:
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?
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:
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:
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:
If you support setting up mandates by phone:
If your merchant is passing you bank details, are you doing modulus checking?
Are you aware that we only expose the last 2 digits from bank account when a customer switches bank account?
How are you creating payments? Do you create individual payment requests or subscriptions?
Can the merchant create a payment as soon as a customer has set up a mandate?
Do you allow merchants to schedule payments in advance?
Do you enable merchants to collect both one-off and recurring payments?
For single one-off payments, do you store our mandate ID?
Are you aware of our maximum £5k limit per transaction?
Do you allow merchants to retry payments that fail? (more details)
What happens when payments fail? For example:
Do you use idempotency keys to avoid duplicate payments being created? (more details)
Are you charging app fees? (more details)
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?
Are you making sure to only enable refunds until at least 2 days after customer is charged?
Do you know that we can still refund a payment even after the mandate has been cancelled?
How are you handling reconciliation when we pay out to your user? For example: (more details)
Have you tested your integration in the sandbox before going live?
Have you connected a separate RPN account to your app?
Have you tested payments with your live account?
Have you tested the redirect URL flow?
How does your integration handle API errors?
Are you aware of our API rate-limiting?
Are you using the correct RPN logo?
Is it clearly visible how to sign up to RPN? For example:
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.
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.
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:
You can find more country-specific bank details in our appendix.
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.