Backend
You need to follow some steps so your LiveApp can communicate properly with our Backend.
In addition to the following steps, Ledger also needs to know:
- The base url of the API,
- Any backend authentication information to use the api (token in header…)
Endpoints needed for Sell
In order to communicate with Ledger’s backend, you must provide standardised APIs for Ledger’s Sell aggregator services to call.
You will find all the information regarding the Sell endpoints here: https://exchange-integration-sell.redoc.ly/, as well as additional details on some endpoints below.
There are 5 endpoints needed for the Sell integration:
- To get the list of available fiat, country, payment, method and amount: /capabilities.
- To get the list of available crypto-currencies: /crypto-currencies.
- To get the fiat amount that the user should expect -after all fees are deducted - as a payment for the given crypto amount: /quotes.
- To get the URL of the off-ramp widget to which the user will be redirected. /sell-redirect.
- To create a signed sell binary payload: /sell.
Additional Details: POST /sell
Example request payload:
{
"quoteId": "string",
"amount": "string",
"refundAddress": "string",
"fromCryptoCurrency": "string",
"toFiatCurrency": "string",
"payloadCryptoCurrency": "string",
"nonce": "number"
}
An example response from the your backend to the Ledger backend
{
"sellId": "SELL-ID-165940",
"payinAddress": "0xa0b86991c627e936c1d19d4a2e90a2ce3606eb48",
"createdAt": "2030-05-26T14:13:39",
"providerFees": "0.0001",
"referralFees": "0.0001",
"payoutNetworkFees": "0.0002",
"providerSig": {
"payload": "CgUweGZmZhoFMHhmZmYqBTB4ZmZmOgNCVENCA0JBVEoIMTIwMDAwMDBSCDExNTAwMDAwWhF2ZXJ5IGxvbmd1ZSBub25jZQ==",
"signature": "MEUCIBRm4PrdgRw0aBwRepuOGGRmR/YPcCoyKNJ7UDjFO030AiEA/VT0anolum0a3X/9lGPeovZHqzeDG9brcUB4zhYmwbs="
}
}
Your Protobuf message should have the following structure:
syntax = "proto3";
package ledger_sell;
// (coefficient) * 10^(- exponent)
message UDecimal {
bytes coefficient = 1;
uint32 exponent = 2;
}
message NewSellResponse {
string trader_email = 1;
string in_currency = 2;
bytes in_amount = 3;
string in_address = 4;
string out_currency = 5;
UDecimal out_amount = 6;
bytes device_transaction_id = 7;
}
in_amount
amount must be in the lowest unit of the coin, encoded into a 16 bytes array in big endian.
- 1 BTC would be
0x5F5E100
(100000000 in hexadecimal). The smallest unit is a satoshi which is10^-8
BTC. So multiply 1 BTC by10^8
→0x5F5E100
. And0x5F5E100
encoded into a 16 bytes array in big endian is[0x00, ... 0x00, 0x05, 0xF5, 0xE1, 0x00]
. - 2 ETH would be
0x1BC16D674EC80000
(or 2000000000000000000). The smallest unit is a wei which is10^-18
ETH. So multiply 2 ETH by10^18
→0x1BC16D674EC80000
. And0x1BC16D674EC80000
encoded into a 16 bytes array in big endian is[0x00, ... 0x00, 0x1B, 0xC1, 0x6D, 0x67, 0x4E, 0xC8, 0x00, 0x00]
.
Ledger IDs mapping
To comply with our integration policies, the LedgerId format must be used (by your /crypto-currencies
and /capabilities
endpoints) and accepted (by your /quote
endpoint and widgetURL) to accurately identify tokens and coins.
You can refer to this API to obtain Ledger IDs. You can use the search
parameter to find a specific coin or token. For instance, to search for USDT, you can use this request. The API returns paginated results: to access the next pages, include cursor=<value of the "X-Ledger-Next" header>
in your request. Further information is available in the response headers.
Example: let’s say your support AAVE on Ethereum mainnet. The LedgerID for AAVE on Ethereum is ethereum/erc20/aave
(as we can see here). This means:
- Your
/crypto-currencies
endpoint must includeethereum/erc20/aave
in its response (along with the other coins/tokens you support), so we know that you support AAVE on Ethereum. - Your
/capabilities
endpoint must includeethereum/erc20/aave
in its response (along with the other coins/tokens you support). - Your
/quote
endpoint must acceptethereum/erc20/aave
as a value for thefrom
parameter. - Your widgetURL must accept
ethereum/erc20/aave
as a value for thefrom
parameter.
Real-time Status Updates:
To ensure effective communication with Ledger’s backend, you are required to implement a webhook /transaction/{sellId}/status update mechanism. This should notify Ledger real-time whenever there is a change in the status of a transaction. Ledger will provide partners with the necessary webhook URL or address to which the status updates should be sent.
Additional Details: POST /transaction/{sellId}/status
status
:FINISHED
: Trade has been completed successfully (user has received payout transaction).EXPIRED
: Payin transaction was not received in time, trade is cancelled. User will be refunded if payin transaction is received afterwards.ON_HOLD
: Trade has been put on hold (eg: for KYC reasons). User must contact support.PENDING
: Trade is in progress (provider is waiting to receive payin transaction, or user is waiting to receive payout transaction)TRANSFER_IN_COMPLETED
: Transfer in completed and waiting for transfer out to be sent.REFUNDED
: Trade has been cancelled, refund transaction has been successfully received by user.UNKNOWN
: Trade is in unknown state. User must contact support.
Additionally, if your backend needs to know the transaction status from our system (e.g., whether the transaction was approved or rejected by the user’s device), we provide an optional endpoint /transaction/{sellId}/status
Webhooks must be sent only after your Front-End has called the Sell method of the Exchange SDK. Sending a webhook beforehand will result in an error 404 (transaction not found), as the Sell ID included the webhook payload won’t be recognized by Ledger at that point.
Payload & Signature
Here is a little diagram to explain how the payload
and the signature
are generated:

payload
: the trade parameters are assembled in a protobuf message. This message is binary encoded to produce a Byte Array. This Byte Array is then base64Url encoded, resultingpayload
field.signature
: The payload is signed using ES256 provider’s private key, generating a Byte Array. This Signature Byte Array is then base64Url encoded, producing the Signature field. (more details).
If you need help with the signing process, please refer to our code examples.
Generate Private/Public Key
-
Private Key: Use the following command to generate the private key:
openssl ecparam -name secp256k1 -genkey -noout -out sample-priv-key-secp256k1.pem
-
Public Key: Derive the public key from the private key using the command:
openssl ec -in sample-priv-key-secp256k1.pem -pubout > sample-pub-key-secp256k1.pem
Verify Key Information
To inspect the contents of the private key file:
openssl ec -text -in sample-priv-key-secp256k1.pem
Repeat this process for both the production and test environments to generate two distinct key pairs.
Signature usage
- Payload and Signature
From the provider to Ledger Live. The payload is a protobuf message containing the trade data. It is generated by the provider and sent to Ledger Live.
type Payload struct {
trader_email,
in_currency,
in_amount,
in_address,
out_currency,
out_amount,
device_transaction_id,
}
R, S := Sign(payload, privKey)
- Validate
Exchange app checks and validates the payload (signature and content).
// Compare nonce in payload
Verify((R, S), payload, pubKey)
- Display
Exchange app requests approval to the user by displaying the operation summary on screen.
Validate?
Send currency_from amount_to_provider
Receive currency_to amount_to_wallet
Input field: nonce
A nonce field will be passed as a parameter of the /sell endpoint.
It is a 32 bytes nonce which is generated by the hardware wallet to avoid replay attacks.
It will be base 64 URL encoded before being sent to the /sell endpoint.
Output field: providerSig
The real return value of the /sell endpoint is the providerSig
field with the JSON Web Signature (JWS) in compact form within:
providerSig.payload
- base64 URL of the binary serialized protobuf message.NewTransactionResponse.providerSig.signature
- base64 URL of the ES256 signature of providerSig.payload. More details in the JWS signature section.
JWS-Signature
The output field is providerSig
, and is based on the flattened JWS JSON, without the optional protected
and header
parameters.
The algorithms supported by Ledger are ES256 with secp256k1 or secp256r1 curve. There is no need to specify them into the header
,
as we will store your public key with this information in our system.
As specified in the JWA spec, the signature is a concatenation of the (R, S) pair in a 64 octets array, and not the ASN1 format.
The JWS RFC requires the signature to be computed on: <BASE64URL_HEADER>.<BASE64URL_PAYLOAD>
. As we don’t require any header
, the signature has to be computed on: .<BASE64URL_PAYLOAD>
.