Use Adyen Tokenization to Implement Subscriptions in .NET

by Kwok He Chu, Developer Advocate at Adyen

Recurring payments, top-ups, and subscriptions are popular payment flows used by lots of merchants in various industries such as streaming services, Software as a Service (SaaS), and subscription services. In this blog post, we go over the technical details of tokenizing payment details and issuing payment requests on behalf of a shopper.

In this article…
We’ll explain the concept of Adyen tokenization, its usage, and how you can use tokenization to implement a recurring billing service for your customers. This post comes with an example integration in .NET that can be found on GitHub. Even though we use .NET in this example, the concepts apply to other programming languages as well.

Payment Service at Adyen

Adyen offers payment services for businesses that operate with subscription models and other recurring payments. We receive payment requests from our merchants, and our systems try to honor the payment authorization. Depending on your use case, the implementation for subscriptions may look differently. Think about the following questions:

  • Do you want to charge the shopper for the first month?
  • When do you want to charge the shopper?
  • What happens when the transaction fails (e.g. insufficient balance or other reasons)?

Let’s take a look at these questions in this blog.

If you don’t want to implement and maintain a subscription billing service yourself, we have partnerships with third-providers that offer these types of billing services. You can find our partners on our website.


With Adyen, you can securely store one or more payment details per shopper in the form of tokens. This process is what we refer to as tokenization. There are three ways you can use tokens:

  • One-off payments: One-off transactions where the cardholder can pay using their previously saved payment details. We refer to this recurring process model in our API as “CardOnFile”.
  • Subscriptions: Recurring transactions made at regular intervals for a product or a service. We refer to this recurring process model in our API as “Subscription”.
  • Automatic top-ups (and other non-fixed schedule contracts): Contracts that occur on a non-fixed schedule using stored card details. This includes automatic top-ups when the cardholder’s balance drops below a certain amount. We refer to this recurring process model in our API as “UnscheduledCardOnFile”.

Subscription Flow

In the next section, we look at the subscription use case and the flow of events that involve the cardholder, the merchant, and the Adyen platform.

1. After a Customer Initiated Transaction (CIT) we collect payment information from the cardholder.

2. The merchant sends an initial authorisation request to Adyen for tokenization.

3. Adyen replies with the authorisation result.

Note: After step 1, the shopper may be required to perform a Strong Customer Authentication (SCA) step where the shopper authenticates to the issuing bank. SCA is a European requirement introduced to make online payments more secure and reduce fraud. This requirement applies to online payments made in the European Economic Area (EEA), Monaco, and the UK. These levels of authentication involve asking customers for two of the three following: something they know, something they own, and something they are. For more details visit this self-service guide.

4. At a later point in time, Adyen sends the token and the shopper reference through an asynchronous notification (webhook) to the merchant. The merchant should save this token and the shopper reference. These are used to make future payment requests for every billing event.

5. The merchant can now, at a set interval, initiate a payment request on behalf of the customer using the token and the shopper reference. This is known as a Merchant Initiated Transaction (MIT).

Finally, the merchant receives an asynchronous notification (webhook) with the outcome of the payment request.

Let’s see this in action in our .NET integration example. In our example integration we’ve implemented two views.

  • One view allows the shopper to tokenize their payment details in the payment request.
  • Another separate view allows an administrator to make payments on behalf of the shopper. Note that in a production environment, you would automate this process. For demo purposes, we allow an administrator to make immediate payments on behalf of the shopper with the press of a button.

Initial payment request

The initial authorisation request is performed using the sessions endpoint. The merchant sends a payment request to Adyen, in our case using the .NET library. This is what the C# request looks like:

/// In CheckoutClient.cs
public async Task<CreateCheckoutSessionResponse> CheckoutSessionsAsync(string shopperReference, CancellationToken cancellationToken)
// Generate a unique Guid for your order.
var orderRef = Guid.NewGuid();

// The sessionRequest holds the data you need to send to Adyen.
var sessionsRequest = new CreateCheckoutSessionRequest();
// Set the merchant account.
sessionsRequest.MerchantAccount = _merchantAccount;
// Make a "0" payment. See Zero Auth section below.
var amount = new Amount("EUR", 0);
sessionsRequest.Amount = amount;
sessionsRequest.Reference = orderRef.ToString();

// It's good practice to send where this request came from
sessionsRequest.Channel = CreateCheckoutSessionRequest.ChannelEnum.Web;

// Specify the Shopper Interaction
sessionsRequest.ShopperInteraction = CreateCheckoutSessionRequest.ShopperInteractionEnum.Ecommerce;

// Set the RecurringProcessingModel to Subscription
sessionsRequest.RecurringProcessingModel = CreateCheckoutSessionRequest.RecurringProcessingModelEnum.Subscription;

// Enable recurring
sessionsRequest.EnableRecurring = true;

// This is an Id that uniquely identifies your shopper.
sessionsRequest.ShopperReference = shopperReference;
// Specify the returnUrl that is required for 3ds2 redirect flow
sessionsRequest.ReturnUrl = $"https://localhost:5001/redirect?orderRef={orderRef}";
// Sends the requests and logs/returns its response.
var sessionResponse = await _checkout.SessionsAsync(sessionsRequest);
return sessionResponse;
catch (Adyen.HttpClient.HttpClientException e)
_logger.LogError($"Request for Payments failed::\n{e.ResponseBody}\n");

Note (out-of-scope): For India, we have to specify a Mandate field. See the documentation for more information on which fields to include.

Now that the shopper’s payment details are collected, let’s take a look at how Adyen authorizes the initial payment request and generates the token. One thing you notice in this example, is that the merchant sends a value of “0” (zero) to Adyen.

This is known as Zero Auth and brings us to the next topic: Dynamic Zero Auth.

Dynamic Zero Auth

A typical use case is when merchants need to verify the card details and account holder without charging the shopper. This might be at the start of a trial subscription, or for a pay-as-you-go model where the shopper is only charged for what is used. By sending a Zero Auth transaction to Adyen, we can do an authorisation with a zero-amount. When this Zero-Auth is accepted, it serves as a validation to the merchant that the shopper is legitimate.

However, Zero-Auths aren’t supported by every issuer. Some issuers require specific values for these authorisations. iDeal, for example, requires an amount greater than 0. Adyen takes care of this complexity by introducing Dynamic Zero Auth (also known as Dynamic Card Validation) to prevent the refusal by automatically adjusting the value to the correct amount for the authorisation.

Finally, Adyen stores the card details safely and initializes a token (also known as the “recurringDetailReference”) that the merchant will be using to make future recurring payment requests.

The tokens never expire but they can be disabled manually through the disable API endpoint.

Making payments on behalf of the shopper

When you are ready to charge the shopper for a subscription, this is what the request looks like in C#.

/// In CheckoutClient.cs
public async Task<PaymentResponse> MakePaymentAsync(string shopperReference, string recurringDetailReference, CancellationToken cancellationToken)
// The payment amount.
var amount = new Amount("EUR", 1199);
var details = new DefaultPaymentMethodDetails
// The token (also known as: recurringDetailReference).
StoredPaymentMethodId = recurringDetailReference

var paymentsRequest = new PaymentRequest
Reference = Guid.NewGuid().ToString(),
Amount = amount,
MerchantAccount = _merchantAccount,
// Set the interaction to ContAuth.
ShopperInteraction = PaymentRequest.ShopperInteractionEnum.ContAuth,
// Set the Subscription model.
RecurringProcessingModel = PaymentRequest.RecurringProcessingModelEnum.Subscription,
// The unique ShopperReference.
ShopperReference = shopperReference,
PaymentMethod = details
// Make a payment on behalf of your shopper.
var paymentResponse = await _checkout.PaymentsAsync(paymentsRequest);
return paymentResponse;
catch (Adyen.HttpClient.HttpClientException e)
_logger.LogError($"Request for Payments failed::\n{e.ResponseBody}\n");

If the payment succeeds, the customer is charged and the service is delivered. But what if the payment request fails? This brings us to the next two topics: Real-time Account Updater and Auto Rescue.

Account Updater

When you submit a payment that is refused for certain refusal reasons (e.g. expired card), the Adyen Real Time Account Updater instantly checks for updated card details with the card schemes (Visa, Mastercard, etc.). If there’s an update, the payment is immediately retried with the updated card details. This all happens as the payment is being processed and appears as a single transaction. The token is also automatically updated to ensure it refers to the newest details.

Adyen also automatically handles cases such as expired cards. The Adyen Batch Account Updater automatically looks up the latest card that belongs to this shopper so we can continue charging the shopper even though their old card expired.

Auto Rescue

Adyen’s Auto Rescue automatically retries refused or charged back shopper-not-present transactions such as subscription renewals. It uses smart logic to decide which payments can succeed when retried later, and performs these retries at optimal times.

A payment can be refused for many reasons. In some cases, when the shopper’s account has insufficient funds, the payment may still succeed when submitted again at a later point in time. In other scenarios, when the shopper’s account has been closed, the payment is declined permanently.

Auto Rescue schedules retries for refused or charged back payments that have a chance of succeeding. It may take several retry attempts to rescue a payment. These attempts occur within a rescue window.

Obtaining the token

Following a successful tokenization request, Adyen sends the token via an asynchronous webhook notification. Let’s take a look at how we consume the webhook notification and obtain the token (referred here as “recurringDetailReference”) in the code. This is what the C# webhook controller looks like:

/// In WebhookController.cs
public ActionResult<string> ReceiveWebhooksAsync(NotificationRequest notificationRequest)
var hmacValidator = new HmacValidator();

foreach(NotificationRequestItemContainer container in notificationRequest.NotificationItemContainers)
// We always recommend to activate HMAC validation in the webhooks for security reasons.
// Read more here: &
if (!hmacValidator.IsValidHmac(container.NotificationItem, _hmacKey))
return BadRequest("[not accepted invalid hmac key]");
// Get the recurringDetailReference and shopperReference from the additionalData property in the webhook. Note that the shopperReference is a uniqueId that the merchant has to provide Adyen
// We store it, so that we can make payment requests on behalf of the shopper in the future.
if (container.NotificationItem.AdditionalData.TryGetValue("recurring.recurringDetailReference", out string recurringDetailReference))
if (container.NotificationItem.AdditionalData.TryGetValue("recurring.shopperReference", out string shopperReference))
_repository.Upsert(container.NotificationItem.PaymentMethod, shopperReference, recurringDetailReference);
catch (Exception e)
_logger.LogError($"Error while calculating HMAC signature::\n{e}\n");
return Ok("[accepted]");


You can find our .NET integration-example on GitHub. Start the application by following the steps in the readme. Once the application is started, you’ll find two views here: the shopper view and the admin panel.

  1. In the shopper view, the shopper can purchase a music subscription. This starts the tokenization process.
  2. In the admin panel, an administrator can execute a payment request on behalf of the shopper (MIT) or choose to disable an existing token.

You can find the stored tokens in the admin panel. Keep in mind that the application keeps the tokens in a local memory cache. Once you restart the application, the tokens are lost.

Our sample application provides you with an admin panel to make payment requests on behalf of the shopper for demo purposes only. On production it would obviously not be the case: integrate the business logic and use the .NET library to bill the customer at predefined intervals by initiating the payment request using the token and the associated shopper reference.

Common Issues and Troubleshooting

While implementing this myself, I ran into several errors. I’ve highlighted some of the most common issues below for troubleshooting purposes.

First, let’s make sure you enable “Recurring details” in Webhooks: Under Additional Data in your Customer Area , make sure you have “Recurring details” (under “Payments”) enabled to receive the notification which contains your recurringDetailReference. This reference is used to make future payment requests. Below, you can find examples of what a successful and unsuccessful webhook look like.

Successful webhook

"live": "false",
"notificationItems": [
"NotificationRequestItem": {
"additionalData": {
"authCode": "004544",
"avsResult": "5 No AVS data provided",
"cardSummary": "1111",
"PaymentAccountReference": "mX1Krp7hMaAxx8WlaS7Xxb5tXXMxp",
"checkoutSessionId": "XXD3BF3E066AC7E749",
"authorisationMid": "50",
"checkout.cardAddedBrand": "visa",
"recurring.firstPspReference": "R6MXS8XF6XXXXX82",
"acquirerAccountCode": "TestPmmAcquirerAccount",
"authorisedAmountValue": "0",
"issuerCountry": "NL",
"cvcResult": "1 Matches",
"expiryDate": "03/2030",
"authorisedAmountCurrency": "EUR",
"recurring.contractTypes": "ONECLICK,RECURRING",
"recurring.recurringDetailReference": "R4F93JFFK6KXWD82",
"recurring.shopperReference": "YOUR_UNIQUE_SHOPPER_ID_IOfW3k9G2PvXFu2j",
"threeds2.cardEnrolled": "false",
"recurringProcessingModel": "CardOnFile",
"paymentMethod": "visa"
"amount": {
"currency": "EUR",
"value": 0
"eventCode": "AUTHORISATION",
"eventDate": "2023-01-16T16:20:51+01:00",
"merchantAccountCode": "TestMerchantAccount",
"merchantReference": "989073cd-b457-4a2e-89d9-6b925ba0a790",
"operations": [
"paymentMethod": "visa",
"pspReference": "XXXXXJ6F3TGLXX12",
"reason": "004544:1111:03/2030",
"success": "true"

Unsuccessful webhook

"amount": {
"currency": "EUR",
"value": 0
"eventCode": "AUTHORISATION",
"eventDate": "2023-01-16T16:20:51+01:00",
"merchantAccountCode": "TestMerchantAccount",
"merchantReference": "989073cd-b457-4a2e-89d9-6b925ba0a790",
"reason":"Insufficient balance on payment",

Error 803: If you get this error after trying to execute a recurring payment request, this means that the stored card details (token), indicated by the recurringDetailReference, is not available for this shopperReference.Either the recurringDetailReference doesn’t exist, or the contract type is not configured properly for this recurringDetailReference.

Double-check which flags you’re sending in the documentation. You can always check which contract types are stored for a specific shopperReference, using the paymentMethods endpoint or the listRecurringDetails endpoint.

Error 422: If you get this error with errorCode ‘14_019’, please make sure you specify the correct recurringDetailReference in your request. The sdkVersion should automatically be provided by the library.

Admin Panel issues: If nothing is showing up in the Admin Panel after making a payment request in the Shopper View. Here’s a list of steps that may help you:

  • Double-check if you’ve done the steps in the readme properly.
  • Is the webhook pointed to the right URL?
  • Have you set up your HMAC Key correctly? If you provide an invalid key, the signature validation-check will fail and the received notification won’t be processed properly.
  • Are you running on localhost? If so, you need a proxy to allow our Adyen servers to reach your endpoint. We have another “blogpost about this.
  • Under “Additional Data” in your Customer Area, make sure you have “Recurring details” (under Payments) to receive the notification which contains your “recurringDetailReference”. This reference is used to make future payment requests.
  • Check your error messages in your console.

If you’ve tried all of the above, and none of the solutions work, open an issue on GitHub.


We’ve seen how you can use Adyen tokenization to implement subscriptions in .NET along with how you can:

  1. Collect the shopper’s payment information
  2. Retrieve the recurringDetailReference asynchronously
  3. Make a payment request using this recurringDetailReference on behalf the shopper.

We’re always looking for ways to improve the integration-example and scale it to multiple languages that are relevant for our merchants. Have a look at our repository today and let us know on Twitter how we can improve. Alternatively, feel free to open an issue on GitHub and we’ll get to it!



Development and design stories from the company building the world’s payments infrastructure.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

Development and design stories from the company building the world’s payments infrastructure.