Signing requests for SCA compliance

If you’re using a Volt Account with Connect, you need to sign each refund and payout request using a private key, to comply with Strong Customer Authentication guidelines, in order for your payment to be straight-through-processed (ie with no further interaction required).  

If your Connect account is provided by one of our partners (eg Clear Junction or ISX) then you do not need to sign your request and you can stop reading here!

Why do you need to do this?

The Payment Services Directive (PSD2) requires that all payments are authenticated using two factors out of the following three:

  • Something you know
  • Something you have
  • Something you are

When creating or approving payments (which includes refunds and payouts) in Fuzebox we use biometric authentication (something you are) on your hardware device (something you have). Payments created via the API are not exempt from this requirement.

How do you do this using the API?

Machine-to-machine communications via API are a little different but we still need to be compliant with PSD2.  So we use the standard username and password authentication (as the “something you know” factor) and a private key to sign each request (the “something you have” factor).

We use JWS (JSON Web Signature) as the signature method for each request.

What happens if you don't sign each API request?

Your request won’t be processed for payment automatically, but will instead appear in a queue for processing in Fuzebox.  You will need to login to Fuzebox and authorise the request using a passkey stored on your computer or mobile device. This can be done in bulk or for each individual payment.

Where can you test this?

Importantly, this can only be setup and tested on production.   Please ensure you conduct a small test on refunds and payouts as required, on the production environment, while you are conducting your production payment tests.

Getting started

The first thing you’ll need to do is create a pair of private / public keys and share the public one with Volt.  Then, for each request, create a JWT (JSON Web Token) which includes the signature and include that in the request header.

Create private / public keys

Technical specifications

  • We support public keys in the following formats: PKCS1, PKCS8 and x509.
  • The public key length should be between 2048 and 4096 bytes.

How to generate your keys

You can generate your private / public key pair using a terminal which supports the openssl command, and by following the appropriate instructions below.

  • X509
  • PKCS1
  • PKCS8
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
openssl genrsa -f4 -out private.txt 4096

openssl rsa -in private.txt -outform PEM -pubout -out public.txt
openssl genrsa -f4 -out private.txt 4096 

openssl pkcs8 -topk8 -inform pem -in private.txt -outform PEM -nocrypt -out private8.txt 

openssl rsa -in private8.txt -outform PEM -pubout -out public.txt

Send your public key to Volt

Please send the public key by email to support@volt.io.

Once you send the public key to us, we will email you an ID, a UUID which you’ll need to use when generating your signed token on each refund or payout request, so please keep this ID safe.

Example of an ID is 901659f9-c0fd-4d2e-82b8-a55f51f80d73

You should never share your private key with Volt, or with any third party.  If you believe your private key may be compromised, you should immediately inform Volt, generate new a key pair and send us the new public key.

Create a JWT token for each request

To sign each request, you will need to provide a JWT token in the header of that request.  To generate a signed JWT token, you’ll need your private key and the ID of your public key, plus the payload (the JSON body) of the POST request for your refund or payout.

Creating your JWT token

You need to create two parts for your JWT token, the header and the signature.  The header tells us how to process the JWT token and the signature is the part that we’ll validate against the public key.

The token header segment

The header should be created first as a JSON object as below, where

  • alg – represents the algorithm used to encode/decode the token, in this case it’s RS256 (the Rsa+Sha256 algorithm)
  • typ – should be “JWT
  • kid – the id of the public key that we supplied you, which will be in UUID form
Example JSON object
{ 
  "alg": "RS256", 
  "typ": "JWT", 
  "kid": "ce161c49-4373-4b07-82fa-217998f6b3e8" 
}
Encoding the header

Once you’ve created the JSON object as per the example above, you should remove any extra spaces or line breaks, then encode it using the base64UrlEncoded algorithm, as per the example below.

Example code to encode the header
tokenHeader = '{"alg": "RS256","typ":"JWT","kid":"ce161c49-4373-4b07-82fa-217998f6b3e8"}'

encodedHeader = base64UrlEncode(tokenHeader)

… which should then look like the example below.

Example encoded header
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNlMTYxYzQ5LTQzNzMtNGIwNy04MmZhLTIxNzk5OGY2YjNlOCJ9

The signature segment

To create the signature segment, we need to use the algorithm that is specified in the header and the private key.  This is where you’ll need a copy of the payload you’re sending in the POST request, because that’s what you’re actually signing here.

In this case we’re using the RSA+SHA256 algorithm and would use the sample code below, utilising the already encoded header plus a base64UrlEncoded version of the JSON payload you will be sending in the refund or payout request.  You should not change the payload after generating the signature as this will invalidate the signed token.

Remove whitespace to improve validation

Please remove any line breaks or extra spaces in the request payload before using the base64UrlEncode function, this will ensure that your signature matches the way we’ll validate it.  For example, in the case of refunds, you should base64UrlEncode('{"amount":1,"externalReference":"my-external-reference"}')

Example code to generate signature
tokenHeader = '{"alg": "RS256","typ":"JWT","kid":"ce161c49-4373-4b07-82fa-217998f6b3e8"}'

requestPayload = '{"amount":1,"externalReference":"my-external-reference"}'

signature = RSASHA256( base64UrlEncode(tokenHeader) + "." + base64UrlEncode(requestPayload), privateKey )

If all is good you should get a signature which looks like this…

Example signature
oUjLLtQigBniTiYswfE0JAjMiYXtIlNtVi1Lr1jqBx103vXgVtEdWApUMpG3wze3qVXD_APA2Sk8oLV4DGeBb5pN7yRGeCdxoV3IsikCVs6rn2Q2Jat-bReQMX39F7-Rpn7RznjHUsyWWcNbDKy1wRFcEnDJBVdb_1lKdFBPWaKMkB1Yd8t8X2va6mq7pJXPAMS36Gwc37vULZvdw4D-49r8mcbEGnNXwkcuZ08hMk4UsmM0kxLeNcrVD3wZtuU0N43u1trlPnuX9RDOOh9Gz0fEH1fwxdveAZaMOOWr7IPHBeV8nZXHxt1lpwJ-dpAsSDMvCFhr-MHuOQDDdqsfhQ

Build your JWT token

To build the final JWT token to sign your request, take the encoded header and join it to the signature as follows.

Example code to build the JWT token
JWT = base64UrlEncode(header) + ".." + signature

So, using the examples given in this page, your final token should look like the example below:

Example final JWT token
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNlMTYxYzQ5LTQzNzMtNGIwNy04MmZhLTIxNzk5OGY2YjNlOCJ9..oUjLLtQigBniTiYswfE0JAjMiYXtIlNtVi1Lr1jqBx103vXgVtEdWApUMpG3wze3qVXD_APA2Sk8oLV4DGeBb5pN7yRGeCdxoV3IsikCVs6rn2Q2Jat-bReQMX39F7-Rpn7RznjHUsyWWcNbDKy1wRFcEnDJBVdb_1lKdFBPWaKMkB1Yd8t8X2va6mq7pJXPAMS36Gwc37vULZvdw4D-49r8mcbEGnNXwkcuZ08hMk4UsmM0kxLeNcrVD3wZtuU0N43u1trlPnuX9RDOOh9Gz0fEH1fwxdveAZaMOOWr7IPHBeV8nZXHxt1lpwJ-dpAsSDMvCFhr-MHuOQDDdqsfhQ

Sign your request

Now you have your request payload and a JWT token which signs that payload, all you need to do is add a JWS header to each request, which contains the JWT token you created.

Header name : X-JWS-Signature

Example header
X-JWS-Signature: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNlMTYxYzQ5LTQzNzMtNGIwNy04MmZhLTIxNzk5OGY2YjNlOCJ9..oUjLLtQigBniTiYswfE0JAjMiYXtIlNtVi1Lr1jqBx103vXgVtEdWApUMpG3wze3qVXD_APA2Sk8oLV4DGeBb5pN7yRGeCdxoV3IsikCVs6rn2Q2Jat-bReQMX39F7-Rpn7RznjHUsyWWcNbDKy1wRFcEnDJBVdb_1lKdFBPWaKMkB1Yd8t8X2va6mq7pJXPAMS36Gwc37vULZvdw4D-49r8mcbEGnNXwkcuZ08hMk4UsmM0kxLeNcrVD3wZtuU0N43u1trlPnuX9RDOOh9Gz0fEH1fwxdveAZaMOOWr7IPHBeV8nZXHxt1lpwJ-dpAsSDMvCFhr-MHuOQDDdqsfhQ