Near Field Communication (NFC) tags can be cards, phones, bracelets, or similar. They can store data and are commonly used for contactless payments, loyalty programs, festival entrance, and more. By sending a Terminal API card acquisition request to your Adyen payment terminal, you can identify the type of NFC tag, or read and write data to it.
If you prefer to use a dedicated Android app for specific NFC operations, it is possible to hide the Adyen UI and keep your app's UI in the foreground. You can also create a session to perform multiple requests as a single user interaction. Additionally, you can read and write counter registers to track how many times an NFC tag has been used and prevent abuse.
To understand how you can use NFC tags, here are some examples:
- Enroll shoppers in a loyalty program.
- Identify a customer when entering an area such as festival grounds or a parking garage.
- Check the balance and top up NFC tags such as loyalty and transport cards, or festival tags used to pay for drinks.
- Define multiple behaviors of the NFC tag. For example, use the tag to enter one event and reuse it to pay for drinks at a different event.
Requirements
Before you begin, take into account the following requirements, limitations, and preparations.
| Requirement | Description |
|---|---|
| Integration type | A Terminal API integration with payment terminals. |
| Hardware | The payment terminal model must support the NFC tags that you want to use. |
| Limitations | Note the following:
|
| Setup steps | Before you begin, provide our Support Team with the following information so that the team can create your NFC configuration:
|
How it works
To use NFC tags on Adyen payment terminals:
- Make a card acquisition request to identify, read, or write to an NFC tag.
For more complex use cases that require multiple requests, see Create a session.
- The terminal prompts the user to present their NFC tag or NFC-enabled card.
- The user presents their NFC tag by tapping, inserting, or swiping.
- The terminal makes one or multiple read/write requests to the NFC tag.
While this is going on, the terminal shows the One moment loading screen.If you want to use a dedicated Android app to process NFC operations, it is possible to hide the Adyen UI and keep your app's UI in the foreground.
Identify the NFC tag type
You can use the card acquisition request to get the unique identifier and the type of the NFC tag. You can use this identifier, for example, to check if the NFC tag exists in your system or if it is still valid.
-
Create an
OperationJSON object withOperation.Type: NFCReadUID: -
Encode the
OperationJSON object to Base64. You will pass the resulting string inSaleData.SaleToPOIData. -
Make a card acquisition request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to CardAcquisition.Parameter Required Description ProtocolVersion
3.0 MessageClass
Service MessageCategory
CardAcquisition MessageType
Request ServiceID
Your unique ID for this request, consisting of 1-10 alphanumeric characters. Must be unique within the last 48 hours for the terminal ( POIID) being used.SaleID
Your unique ID for the POS system component to send this request from. POIID
The unique ID of the terminal to send this request to. Format: [device model]-[serial number]. -
The CardAcquisitionRequest.SaleData object with:
Parameter Required Description SaleTransactionID
An object with: TransactionID: Your reference to identify the transaction. We recommend using a unique value.TimeStamp: The date and time of the request in UTC format.
SaleToPOIData
The Base64-encoded OperationJSON object. -
The CardAcquisitionRequest.CardAcquisitionTransaction object with:
Parameter Required Description TotalAmountThe transaction amount. You can omit the TotalAmountfield when getting theUIDof the NFC tag.If you omitTotalAmount, you still have to include an emptyCardAcquisitionTransactionfield in the request.
The following example shows a card acquisition request to identify the NFC tag.
-
-
In the
AdditionalResponseof the CardAcquisitionResponse note:NFC.uid: The unique identifier of the NFC tag.NFC.variant: The type of the NFC tag, for example mf_classic for MIFARE Classic.
After you have identified the NFC tag, the loading screen continues to show until you send an enable service request (see the next step).
-
To stop the loading screen, make an enable service request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to EnableService.Parameter Required Description ProtocolVersion
3.0 MessageClass
Service MessageCategory
EnableService MessageType
Request ServiceID
Your unique ID for this request, consisting of 1-10 alphanumeric characters. Must be unique within the last 48 hours for the terminal ( POIID) being used.SaleID
Your unique ID for the POS system component to send this request from. POIID
The unique ID of the terminal to send this request to. Format: [device model]-[serial number]. -
The EnableServiceRequest object with:
Parameter Required Description TransactionAction
AbortTransaction. DisplayOutputOptional object to show your own message and an 'Approved' icon (green check mark) or a 'Declined' icon (red cross). If you omitDisplayOutput, the terminal shows Canceled, a red cross
, and Transaction canceled. Omit if you have an NFC configuration that hides the Adyen UI while using a dedicated Android app for NFC operations. Otherwise the Adyen UI will be brought to the foreground.
The following example is the basic request, without the
DisplayOutputobject, to stop the card acquisition flow. -
Read data
NFC tags have multiple parts that can store data. Because the data structure is different for every NFC tag type, the request needs to specify where to read the data from. For some tags, you need to include the key to read data from a sector, aid, orpage.
Select a tab to see the parameters that you need to specify to read data from MIFARE Classic, MIFARE DESFire, or MIFARE Ultralight.
Write data
NFC tags have multiple parts that can store data. Because the data structure is different for every NFC tag type, the request needs to specify the sector, aid, or page where to write the data to.
Select a tab to see the parameters that you need to specify to write data to MIFARE Classic, MIFARE DESFire, or MIFARE Ultralight.
Track NFC tag usage with counter registers
MIFARE Ultralight AES is the only NFC tag type that provides hardware counter registers. MIFARE Ultralight AES has three independent 24-bit counters, separate from the tag's regular memory, that track how many times the tag has been used. Because a counter is independent of the tag's memory, tampering with the memory doesn't affect the counter. This means a mismatch between the two can reveal tampering, for example restoring a spent balance. A counter's value can only increase. This prevents bad actors from decreasing the value, resetting it, or setting it to an arbitrary number. Depending on your use case, you can use counter registers for:
- Secure closed-loop cashless spending: Write and read a counter register to detect if a tag's stored balance was rolled back to reuse spent funds, for example on a wristband used to pay for festival drinks.
- Access control and usage limits: Write and read a counter register to enforce a usage limit, for example a single-entry turnstile pass.
- Fast, interruption-free transactions ("anti-tearing"): Write to a counter register so a fast tap either fully registers or doesn't register at all, even if a tag is removed mid-transaction.
In this section you learn how to:
Read counter registers
Reading a counter register returns its current value without changing it. To increase the value, see Write counter registers.
To read the value of a counter register:
-
Create an
OperationJSON object specifying:Parameter Required Description Type
NFCRead: reads the value of the specified counters. Variant
MifareUltralightAES: the type of NFC tag. NFCData
An array with one entry for each counter you want to read. Each counter is independent, so you can use different counters to track different purposes on the same tag, for example one counter for access control and another for purchases. Each entry has: Counter.Index: the counter to read. Valid values: 0, 1, or 2.ultralightAESKey: the key used to authenticate the card.- SKIPAUTH: skips authentication. Use this for cards with free access.
- A 16-byte HEX-encoded key, for example
11223344556677889900AABBCCDDEEFF. - If omitted, the terminal uses the default key:
00000000000000000000000000000000.
ultralightAESKeyId: optional. The key that specifies what part of the card's memory to access, with the same meaning and defaults as for page-based reads and writes. Possible values:- 0: (default)
DataProtKeyauthenticates the card and accesses protected user memory and theUID. - 1:
UIDRetrKeyauthenticates the card and retrieves theUIDand theECDSAsignature.
- 0: (default)
The following example shows an
Operationobject that reads counter registers 0, 1, and 2: -
Encode the
OperationJSON object to Base64. You will pass the resulting string inSaleData.SaleToPOIData. -
Make a card acquisition request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to CardAcquisition. -
The CardAcquisitionRequest.SaleData object with:
Parameter Required Description SaleTransactionID
An object with: TransactionID: your reference to identify the transaction. We recommend using a unique value.TimeStamp: date and time of the request in UTC format.
SaleToPOIData
The Base64-encoded OperationJSON object. -
The CardAcquisitionRequest.CardAcquisitionTransaction object with:
Parameter Required Description TotalAmountThe transaction amount. You can omit the TotalAmountfield when reading the counters.If you omitTotalAmount, you still have to include an emptyCardAcquisitionTransactionfield in the request.
The following example shows a card acquisition request to read counters 0, 1, and 2 on a MifareUltralightAES NFC tag:
-
-
The
AdditionalResponseof the CardAcquisitionResponse is a base64-encoded string that you need to decode to get a JSON object. Note the following fields:NFC.data: the current value of each requested counter, returned as a little-endian HEX string, for example NFC.data.C0=070000. To get the decimal value, reverse the byte order before converting:070000reversed is000007, which is 7.NFC.status: the status of the read action for every counter, for example NFC.status=C1.OKC0.OKC2.OK. IfultralightAESKeyauthentication fails, the individual counter statuses are not returned. Instead, the response contains a singleNFC.status=AUTH_ERROR. See Handle counter error codes.NFC.uid: the unique identifier of the NFC tag.NFC.variant: the type of the NFC tag, in this case MIFARE Ultralight AES.
After you have read the specified counters on the NFC tag, the loading screen continues to show until you send an
enable service request (see the next step). -
To stop the loading screen, make a Terminal API enable service request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to EnableService. -
EnableServiceRequest with:
Parameter Required Description TransactionAction
AbortTransaction. DisplayOutputOptional object to show your own message and an 'Approved' icon (green check mark) or a 'Declined' icon (red cross). If you omitDisplayOutput, the terminal shows Canceled, a red cross
, and Transaction canceled. Omit if you have an NFC configuration that hides the Adyen UI while using a dedicated Android app for NFC operations. Otherwise the Adyen UI will be brought to the foreground.
The following example is the basic request, without the
DisplayOutputobject, to stop the card acquisition flow. -
Write counter registers
Writing to a counter register increases its stored value by the amount you specify. You cannot set a counter to a specific value or decrease it. Every write request only adds to the current value.
To write to a counter register:
-
Create an
OperationJSON object specifying:Parameter Required Description Type
NFCWrite: increments the specified counters. Variant
MifareUltralightAES: the type of NFC tag. NFCData
An array with one entry for each counter you want to increment. Each counter is independent, so you can use different counters to track different purposes on the same tag, for example one counter for access control and another for purchases. Each entry has: Counter.Index: the counter to increment. Valid values: 0, 1, or 2.Counter.Value: an integer specifying the amount to increment the counter by. Maximum value: 16777215 (0xFFFFFF, the maximum 24-bit value). Negative values are treated as 0 and do not increment the counter.ultralightAESKey: the key used to authenticate the card.- SKIPAUTH: skips authentication. Use this for cards with free access.
- A 16-byte HEX-encoded key, for example
11223344556677889900AABBCCDDEEFF. - If omitted, the terminal uses the default key:
00000000000000000000000000000000.
ultralightAESKeyId: optional. Same meaning and defaults as for Read counter registers.
The following example shows an
Operationobject that increments all three counter registers by 1: -
Encode the
OperationJSON object to Base64. You will pass the resulting string inSaleData.SaleToPOIData. -
Make a card acquisition request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to CardAcquisition. -
The CardAcquisitionRequest.SaleData object with:
Parameter Required Description SaleTransactionID
An object with: TransactionID: your reference to identify the transaction. We recommend using a unique value.TimeStamp: date and time of the request in UTC format.
SaleToPOIData
The Base64-encoded OperationJSON object. -
The CardAcquisitionRequest.CardAcquisitionTransaction object with:
Parameter Required Description TotalAmountThe transaction amount. You can omit the TotalAmountfield when incrementing the counters.If you omitTotalAmount, you still have to include an emptyCardAcquisitionTransactionfield in the request.
The following example shows a card acquisition request to increment counters 0, 1, and 2 by 1 on a MifareUltralightAES NFC tag:
-
-
The
AdditionalResponseof the CardAcquisitionResponse is a base64-encoded string that you need to decode to get a JSON object. Note the following fields:NFC.data: the amount each counter was incremented by, not its new total. It's returned as a little-endian HEX string, for example, NFC.data.C0=010000 means counter 0 was incremented by 1.NFC.status: the status of the increment action for every counter, for example NFC.status=C0.OKC2.OKC1.OK. IfultralightAESKeyauthentication fails, the individual counter statuses are not returned. Instead, the response contains a singleNFC.status=AUTH_ERROR. See Handle counter error codes.NFC.uid: the unique identifier of the NFC tag.NFC.variant: the type of the NFC tag, in this case MIFARE Ultralight AES.
After you have incremented the specified counters on the NFC tag, the loading screen continues to show until you send an
enable service request (see the next step). -
To stop the loading screen, make a Terminal API enable service request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to EnableService. -
EnableServiceRequest with:
Parameter Required Description TransactionAction
AbortTransaction. DisplayOutputOptional object to show your own message and an 'Approved' icon (green check mark) or a 'Declined' icon (red cross). If you omitDisplayOutput, the terminal shows Canceled, a red cross
, and Transaction canceled. Omit if you have an NFC configuration that hides the Adyen UI while using a dedicated Android app for NFC operations. Otherwise the Adyen UI will be brought to the foreground.
The following example is the basic request, without the
DisplayOutputobject, to stop the card acquisition flow. -
Handle counter error codes
The terminal validates counter requests and reports errors in the NFC.status field of the AdditionalResponse.
| Error code | Description |
|---|---|
AUTH_ERROR |
Authentication failed. Check your ultralightAESKey and ultralightAESKeyId. |
SECTOR_OUT_BOUNDS |
The Counter.Index is invalid. Use 0, 1, or 2. |
DATA_SIZE_INCORRECT |
The increment Counter.Value is out of range. Use a value up to 16777215 (0xFFFFFF, the maximum 24-bit value). Only applies to Write counter registers. |
UNSUPPORTED_ACTION |
The request mixed page-based and counter-based operations in the same NFCData array. Send separate requests for each type of operation. |
Create a session
If your use case requires multiple read/write actions on an NFC tag, you can create a session to make multiple requests part of the same user interaction. While the session is in progress, the terminal shows the One moment loading screen.
The flow consists of the following steps:
-
In the first card acquisition request to identify, read, or write NFC tags, you start the session by adding
Session.Type: Begin in theSaleToPOIData.All requests that belong to the session must contain the same
Session.Idin theSaleToPOIData. -
You then send more card acquisition requests for subsequent NFC tag operations.
-
To finish the session:
- In the last NFC tag card acquisition request of the session, you add
Session.Type: End in theSaleToPOIData. - You send an enable service request to ensure the terminal stops showing the loading screen.
- In the last NFC tag card acquisition request of the session, you add
Here is an example of a session that consists of three card acquisition requests, to:
- Start a session and identify the NFC tag type
- Continue the session with a Read data request
- Finish the session with a Write data request.
1. Start a session and identify the NFC tag type
When starting the session, you need specify the Id to connect the requests into a single session. Here's example of how identify the tag and start the session:
-
Create a JSON object with:
Parameter Required Description Session.Type
Begin: starts the session. Session.Id
Your unique reference of the session. Session.Timeout
- How long the One moment screen is shown on the terminal display, in milliseconds.
- If there is no end session request, how long before the session data is deleted and new requests can be sent.
Operation.Type
NFCReadUID: returns the NFC tag identifier and type. -
Encode the JSON object to Base64. You will pass the resulting string in
SaleData.SaleToPOIData. -
Make a card acquisition request with the Base64-encoded string in the
SaleToPOIData.
2. Send read data request with Session.Id
Make sure to include the Session.Id specified in the SaleToPOIData of the first request. Here's example of how to read data from MIFARE Classic as part of the session:
-
Create a JSON object with:
Parameter Required Description Session.Id
Your unique reference of the session, specified in the first request. Operation.Type
NFCRead: reads data from the specified sector. Operation.Variant
MifareClassic: the type of NFC tag. Operation.NFCData
An array with:If you leave theNFCDataarray empty, the terminal will try to read all sectors.sector: specifies what sector on MifareClassic to read from.keyType: specifies what key is used to access a sector, a (default) or b.
-
Encode the JSON object to Base64. You will pass the resulting string in
SaleData.SaleToPOIData. -
Make a card acquisition request with the Base64-encoded string in the
SaleToPOIData.
The following example shows how to read data from MifareClassic NFC tag as part of the session:
3. Write data to the NFC tag and end the session
In the final request of the interaction, make sure to include the same Session.Id and Session.Type: End. Here's example of how to write data to MIFARE Classic and end the session:
-
Create a JSON object with:
Parameter Required Description Session.Type
End: ends the session. Session.Id
Your unique reference of the session. Operation.Type
NFCWrite: reads data from the specified sector. Operation.Variant
MifareClassic: the type of NFC tag. Operation.NFCData
An array with:If you leave theNFCDataarray empty when trying to write, the response will return theUIDand thevariantbut will not write to the card.sector: specifies what sector on MifareClassic to read from.keyType: specifies what key is used to access a sector, a (default) or b.
-
Encode the JSON object to Base64. You will pass the resulting string in
SaleData.SaleToPOIData. -
Make a card acquisition request with the Base64-encoded string in the
SaleToPOIData.
The following example shows how to write data MifareClassic NFC tag and end the session:After you end the session, the loading screen continues to show until you send an enable service request (see the
next step). -
To stop the loading screen, make a Terminal API enable service request, specifying:
-
The standard
SaleToPOIRequest.MessageHeaderobject, withMessageClassset to Service andMessageCategoryset to EnableService. -
EnableServiceRequest with:
Parameter Required Description TransactionAction
AbortTransaction. DisplayOutputOptional object to show your own message and an 'Approved' icon (green check mark) or a 'Declined' icon (red cross). If you omitDisplayOutput, the terminal shows Canceled, a red cross
, and Transaction canceled. Omit if you have an NFC configuration that hides the Adyen UI to make sure the UI does not show.
The following example is the basic request, without the
DisplayOutputobject, to stop the card acquisition flow. -
Hide the Adyen UI during NFC operations
Optionally, if you want to use a dedicated Android app to process NFC operations for stored value cards or for identity cards, you can show your own UI on the terminal display instead of the Adyen UI. This can be useful, for example, for a loyalty card or transport card.
If you want to use this feature, contact our Support Team.
When this feature is enabled and you make a card acquisition request to identify, read, or write to an NFC tag:
-
If the payment terminal detects a stored value or identity card, the Adyen UI is hidden.
-
If the terminal detects a regular payment card during any of the NFC card acquisition flows, the response returns a BLOCK_CARD error. This does not mean that the card itself is blocked, but that it cannot be used for operations with your Android app. We recommend that your system returns a message that instructs the terminal operator to ask the shopper to present a card or tag that can be used with your app.
The following code sample shows the response for a card acquisition request to identify the NFC tag, when the customer presented a regular payment card.
Regular card acquisitions are not affected by this feature and show the Adyen UI as expected.
Supported terminals and NFC tags
The following table shows the payment terminal models that support NFC tags.
Castles Android
| Models | MIFARE Classic | MIFARE DESFire | MIFARE Ultralight | MIFARE Ultralight C | MIFARE Ultralight AES |
|---|---|---|---|---|---|
| AMS1 | ![]() |
![]() |
![]() |
![]() |
![]() |
| S1E | ![]() |
![]() |
![]() |
![]() |
![]() |
| S1E2L | ![]() |
![]() |
![]() |
![]() |
![]() |
| S1F2 | ![]() |
![]() |
![]() |
![]() |
![]() |
| S1F2L | ![]() |
![]() |
![]() |
![]() |
![]() |
| S1U2 | ![]() |
![]() |
![]() |
![]() |
![]() |
Verifone Android
| Models | MIFARE Classic | MIFARE DESFire | MIFARE Ultralight | MIFARE Ultralight C | MIFARE Ultralight AES |
|---|---|---|---|---|---|
| M450 | ![]() |
![]() |
![]() |
![]() |
![]() |
| P630 | ![]() |
![]() |
![]() |
![]() |
![]() |
Engage
Engage terminals cannot authenticate MIFARE Ultralight C tags. This means that reading/writing Ultralight C tags only works on unlocked pages.
| Models | MIFARE Classic | MIFARE DESFire | MIFARE Ultralight | MIFARE Ultralight C | MIFARE Ultralight AES |
|---|---|---|---|---|---|
| e285p | ![]() |
![]() |
![]() |
![]() |
![]() |
| M400 | ![]() |
![]() |
![]() |
![]() |
![]() |
| P400 Plus | ![]() |
![]() |
![]() |
![]() |
![]() |
| V240m Plus | ![]() |
![]() |
![]() |
![]() |
![]() |
| V400c Plus | ![]() |
![]() |
![]() |
![]() |
![]() |
| V400m | ![]() |
![]() |
![]() |
![]() |
![]() |
| UX300 | ![]() |
![]() |
![]() |
![]() |
![]() |
| UX410 | ![]() |
![]() |
![]() |
![]() |
![]() |
Datecs
| Models | MIFARE Classic | MIFARE DESFire | MIFARE Ultralight | MIFARE Ultralight C | MIFARE Ultralight AES |
|---|---|---|---|---|---|
| SFO1 | ![]() |
![]() |
![]() |
![]() |
![]() |
