Use Wise SCA as a C# developer with Azure KeyVault


If you are a .NET developer and want to programmatically access your VISA or bank transfer spendings look no further than Wise (formerly transferwise). Wise offers an API for you to not only check your account balance in code, but also download the list of your transactions, incoming and outgoing. I am using this to keep track of savings and to have an eye on monthly spendings, issuing automatic warnings when the budget for groceries is used up for example. You can even create webhooks for transfers, deposits or issues.

Whatever your reason might be, you will quickly get familiar with the concept. The propably most basic request to Wise is to download your profile, as you will need to use your profileID a lot.

On production, the URL is:

To authenticate yourself Wise offers API Tokens. To create one, log into your wise account, go to settings and click on "API tokens". Click on Add new token and add your token. You just need Read only for now.


With Reveal Key your API key is shown to you. Next: code

Get profile information

Using the modern HttpClient a request has to have Authorization: Bearer {key}

HttpClient client = new HttpClient();           
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);

After setting the header you are good to request your profile:

string myProfileJson = await client.GetStringAsync("");

The resulting JSON unfortunately is malformed, looking like this:

"id": value,
"type": "personal", 
"details": {}

Having an enumeration as root object is tricky. The JSONFormatter of .NET can not handle it. Therefore we need to trim that to work.

myProfileJson = profileJson.TrimStart('[');
myProfileJson = profileJson.TrimEnd(']');

Now having an object as root object, wen can go forward and parse it.

Tip: Having the JSON in clipboard you can use "Edit", "Paste Special", "Paste JSON as classes" in Visual Studio. This creates the class structure automatically, which is ready for usage.

I prefer to wrap classes derived from JSON objects into a static class, as it gives more structure, and classes can repeat themselves without overriding each other.

The resulting class structure should look like this:

public static class GetProfileResponse
        public class Profile
            public int id { get; set; }
            public string type { get; set; }
            public Details details { get; set; }

        public class Details
            public string firstName { get; set; }
            public string lastName { get; set; }
            public string dateOfBirth { get; set; }
            public string phoneNumber { get; set; }
            public object avatar { get; set; }
            public object occupation { get; set; }
            public object occupations { get; set; }
            public int primaryAddress { get; set; }
            public object firstNameInKana { get; set; }
            public object lastNameInKana { get; set; }

Now we can deserialize the JSON and have an instance

var profile = JsonConvert.DeserializeObject<GetProfileResponse.Profile>(myProfileJson);

Congratulations, you have successfully parsed your first wise response!

The whole code for clarity:

HttpClient client = new HttpClient();           
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
string myProfileJson = await client.GetStringAsync("");
myProfileJson = profileJson.TrimStart('[');
myProfileJson = profileJson.TrimEnd(']');
var profile = JsonConvert.DeserializeObject<GetProfileResponse.Profile>(myProfileJson);

Work with SCA

As described in the documentation, the european union recently introduced rules to make payments and payment information more secure, requiring wise to ask 2 factor information from us. A simple API key is not enough anymore to see payment details. The solution of wise is called Strong Customer Authentication. One way we can provide a second authentication next to the first (the API key) is by using a certificate. Certificates have 2 keys: a public and a private one. We give our public key to wise. When we make a request requiring a second factor, wise will reply with a status code 403, a header named "x-2fa-approval-result" with the content "REJECTED", and a second header named "x-2fa-approval" with a GUID inside.


This is where a certificate comes into play. What wise wants us to do is to make the same request again, but this time with 2 new headers: X-2FA-Approval and X-Signature

The former is supposed to contain the the GUID they gave us with x-2fa-approval, the latter should contain the signed hash of the GUID. This signing is performed with the private key. When the request arrives at wise again, but this time with the 2 new headers, they use our public key to check if we successfully signed the GUID, which we can only do if we have the coresponding private key.


Creating a certificate in Azure

Next, lets make a certificate. Log into and locate / create your KeyVault. On the Certificates Tab click on Generate/Import

The key we want is a PEM key, give it a name and a subject. We are good to go with a Self-signed certificate. See the screenshot for reference


Next click Create and wait for the key to show up in your KeyVault. Click the key, click the CURRENT VERSION and click Download in PFX/PEM format

Creating a public key PEM

Unfortunately we have to break the chain of security for a moment here, as KeyVault does not offer to download a public key PEM, which is what Wise demands. Therefore, we need to download the private key PEM, create the public key PEM from it, delete the private key PEM and hope no one saw that. If there is a better option please let me know.

Second bad news: to create the public key PEM from the private key PEM we need to use openssl. The command is:

openssl rsa -in azure.pem -outform PEM -pubout -out public.pem

Where azure.pem is the file you downloaded from azure.

If you are a linux user you can go get a coffee, you have some waiting time until the windows users got this done.

If you are a windows user there is some trouble ahead. As openssl is not a native app on windows, we have to install linux first. Open the Microsoft store on your computer, search for ubuntu and install the subsystem.


Once it is troubleshooted and started, run ubuntu in a CMD. In your windows explorer, you should have a linux icon in the tree. Open it, choose a folder (like /Ubuntu/home/) and copy the azure.PEM to that location. Switch to the same location in your ubuntu CMD and run the openssl command from above.

Now that we have a public.pem we can go ahead and delete the azure.pem, as that private key does not belong on any machine but the KeyVault. On the Wise website, click in settings once again on API tokens, and choose Manage public keys. Upload the public.PEM file. You can delete it from your harddrive now as well.

You are ready to provide the strong customer authentication.

Requesting statement.json with SCA

An URL that requites SCA is the request of a statement.json, for example:

string scaProtectedUrl = "{profileID}/balance-statements/{balanceID}/statement.json?intervalStart=2022-03-01T00:00:00.000Z&intervalEnd=2022-03-16T23:59:59.999Z&type=COMPACT"

As described earlier, trying to request the URL with just a Bearer Authorization is going to result in a x-2fa-approval-result: REJECTED response header, and a GUID provided with x-2fa-approval.

The first thing to sign the GUID is to get our certificate from the KeyVault Certificate store.

Install the NUGET packages Azure.Security.KeyVault.Certificates and Azure.Identity. Then you can query the certificate as X509Certificate2 object.

var client = new CertificateClient(new Uri(kvUri), new DefaultAzureCredential());
X509Certificate2 myCertificate = client.DownloadCertificate(certificateName);

Tip: refer to this tutorial to understand what your kvUri is and how DefaultAzureCredential() works

Next we read the header containing the GUID to sign it

HttpClient client = new HttpClient();           
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
var nonSCAResponse = await client.GetAsync(scaProtectedUrl);
//check for necessity removed for clarity
string approvalCode = string.Join("", firstResponse.Headers.GetValues("x-2fa-approval"));

Now we have a private key certificate (myCertificate) and a GUID (approvalCode) to sign.

RSA rsa = myCertificate.GetRSAPrivateKey();
var codeBytes = Encoding.UTF8.GetBytes(approvalCode);
byte[] signedBytes = rsa.SignData(codeBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
string signedText = Convert.ToBase64String(signedBytes);

The only thing left is to repeat the request, but this time with the X-2FA-Approval and X-Signature headers.

client.DefaultRequestHeaders.Add("X-2FA-Approval", approvalCode);
client.DefaultRequestHeaders.Add("X-Signature", signedText);

Done. Sending the request now returns not only all the transactions from the last 3 month, but also a comforting x-2fa-approval-result: APPROVED header


Note: if i helped you please consider buying me a coffee:

Did you find this article valuable?

Support Jens Caasen by becoming a sponsor. Any amount is appreciated!