Data collection
When your customer requests a withdrawal, usually by entering a destination address, an asset and an amount, you collect its Travel Rule data before you create the transfer. You can use Withdrawal Assist, an Embedded component that shows the fields the transfer needs, or build your own UI on the requirements the API returns. Either way, you only collect what the transfer actually needs.
Each approach takes one call before you create the transfer: a delegate token for the component, or an assessment for your own UI.
Done by Notabene
Only when needed
With the destination address, the asset and the amount.
Your backend requests one for this customer: A1. Get a delegate token.
It asks your customer for what the rules require: A2. Open Withdrawal Assist.
Ask Notabene what this transfer requires: B1. Assess the transfer.
Ask your customer where the funds are going: If no institution is found.
Ask only for what the assessment lists: B2. Collect the required fields.
With the data you collected: Then create the transfer.
- Your customer requests a withdrawal (Your customer, in your app). With the destination address, the asset and the amount.
- Embedded component: Get a delegate token (
POST /api/v1/entities/{entityDID}/delegate-token, Your backend). Your backend requests one for this customer: A1. Get a delegate token. - Embedded component: Open Withdrawal Assist (Your frontend, then your customer). It asks your customer for what the rules require: A2. Open Withdrawal Assist.
- Your own UI: Assess the transfer (
POST /api/v1/transfers/assess, Your backend). Ask Notabene what this transfer requires: B1. Assess the transfer. - Your own UI: If no institution is found (only when needed, Your app). Ask your customer where the funds are going: If no institution is found.
- Your own UI: Collect the required fields (Your app). Ask only for what the assessment lists: B2. Collect the required fields.
- Create the transfer (
POST /api/v1/transfers, Your backend). With the data you collected: Then create the transfer. Continues in: Withdrawals.
Before you start
- An access token for your backend, and a delegate token for your app if it calls Notabene itself, both described in Authentication.
- Your customer registered with Notabene, as described in Customers, so the transfer can refer to them by their customer id.
Whose rules apply
Your own jurisdiction’s rules apply, as set in the dashboard under Authorization Rules › Travel Rule. That is also where you can choose to meet the counterparty’s rules when they ask for more.
Portugal, for example, applies from the first euro and asks for both names and both account numbers. It also asks for one of the originator’s date and place of birth, customer identification, address or national identification.
A. With the Embedded component
Withdrawal Assist is part of Notabene’s JavaScript SDK. It asks your customer only for the fields this transfer needs.
A1. Get a delegate token
Your backend uses its own access token to request a delegate token for this customer, so your access token stays on your server and never reaches the browser.
curl https://sandbox.notabene.id/api/v1/entities/did:web:originator.example:pt/delegate-token \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27" }'const response = await fetch('https://sandbox.notabene.id/api/v1/entities/did:web:originator.example:pt/delegate-token', { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ "customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27" }), }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.post( 'https://sandbox.notabene.id/api/v1/entities/did:web:originator.example:pt/delegate-token', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, json={ "customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27" }, ) answer = None if response.status_code == 204 else response.json()
What to replace
did:web:originator.example:pt: Your institution’s DIDurn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27: The id of your customer
{
"access_token": "eyJhbGciOiJFZERTQSIs",
"token_type": "Bearer",
"expires_in": 3600,
"jti": "c03044be-8e0b-4bbe-9d2c-c7b28ac3aa5f"
}curl https://sandbox.notabene.id/api/v1/entities/did:web:originator.example:pt/delegate-token \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}'const response = await fetch('https://sandbox.notabene.id/api/v1/entities/did:web:originator.example:pt/delegate-token', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}),
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.post(
'https://sandbox.notabene.id/api/v1/entities/did:web:originator.example:pt/delegate-token',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
json={
"customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
},
)
answer = None if response.status_code == 204 else response.json(){
"access_token": "eyJhbGciOiJFZERTQSIs",
"token_type": "Bearer",
"expires_in": 3600,
"jti": "c03044be-8e0b-4bbe-9d2c-c7b28ac3aa5f"
}Pass the access_token to your frontend, which opens the component with it in the next step. A delegate token works for one customer only, so request it for the customer who is signed in. The access token your backend uses is covered in Authentication.
A2. Open Withdrawal Assist
Install @notabene/javascript-sdk 2.24 or later, and create Notabene from its v2.1 entry with a fetchToken function that gets a delegate token from your backend. The SDK calls it at start and again before the token expires.
const { Notabene } = await import("@notabene/javascript-sdk/src/v21");
const notabene = new Notabene({
nodeUrl: "https://sandbox.notabene.id",
// gets a delegate token for the signed-in customer from your backend (A1)
fetchToken: async () => {
const res = await fetch("/api/notabene/delegate-token", { method: "POST" });
return (await res.json()).access_token;
},
});
const withdrawal = notabene.createWithdrawalAssist({
asset: "ETH",
amountDecimal: 0.5,
destination: "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
});
withdrawal.mount("#nb-withdrawal");
const { transfer } = await withdrawal.completion();
const { Notabene } = await import("@notabene/javascript-sdk/src/v21");
const notabene = new Notabene({
nodeUrl: "https://sandbox.notabene.id",
// gets a delegate token for the signed-in customer from your backend (A1)
fetchToken: async () => {
const res = await fetch("/api/notabene/delegate-token", { method: "POST" });
return (await res.json()).access_token;
},
});
const withdrawal = notabene.createWithdrawalAssist({
asset: "ETH",
amountDecimal: 0.5,
destination: "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
});
withdrawal.mount("#nb-withdrawal");
const { transfer } = await withdrawal.completion();Every field is optional, and the component asks your customer for anything you leave out. Use openModal() to open it as a modal, or popup() for a popup window, which many self-hosted wallets need.
The component returns transfer, the body for POST /api/v1/transfers. Your backend adds a ref and creates the transfer with its access token.
Other components
createWithdrawalAssist (Withdrawal Assist)
Use
A withdrawal.
Returns
transfer, to create.
createDepositAssist (Deposit Assist)
Use
A deposit that arrived without Travel Rule data, as in Deposits, funds first.
Returns
transfer, to create with the transaction hash in settlement_id.
createConnectWallet (Connect Wallet)
Use
A proof that your customer owns a self-hosted wallet.
Returns
wallet, the address and the proof to record, as in Wallets.
| Component | Use | Returns |
|---|---|---|
createWithdrawalAssist (Withdrawal Assist) | A withdrawal. | transfer, to create. |
createDepositAssist (Deposit Assist) | A deposit that arrived without Travel Rule data, as in Deposits, funds first. | transfer, to create with the transaction hash in settlement_id. |
createConnectWallet (Connect Wallet) | A proof that your customer owns a self-hosted wallet. | wallet, the address and the proof to record, as in Wallets. |
The options, such as the counterparty types to allow, are in the SDK’s README.
B. With your own UI
The API returns the requirements, and your app shows the fields.
B1. Assess the transfer
Before asking your customer for anything, ask Notabene what this transfer requires. The response tells you whether the Travel Rule applies, which fields you need to collect and which institution holds the destination address. This call does not create anything or contact the counterparty.
curl https://sandbox.notabene.id/api/v1/transfers/assess \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "asset": "ETH", "amount": "0.5", "destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" }'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/assess', { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ "asset": "ETH", "amount": "0.5", "destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" }), }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.post( 'https://sandbox.notabene.id/api/v1/transfers/assess', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, json={ "asset": "ETH", "amount": "0.5", "destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" }, ) answer = None if response.status_code == 204 else response.json()
What to replace
eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed: The destination address your customer entered
{
"travelRule": "Applies",
"isTravelRule": true,
"presentationDefinitionUrl": "https://pd.notabene.id/ivms101/v2/PT-0.json",
"threshold": { "amount": "0", "currency": "EUR" },
"required": [
"originatorName",
"originatorAccountNumber",
"beneficiaryName",
"beneficiaryAccountNumber",
{
"oneOf": [
"originatorDateAndPlaceOfBirth",
"originatorCustomerIdentification",
"originatorGeographicAddress",
"originatorNationalIdentification"
]
}
],
"beneficiaryVasp": {
"did": "did:web:beneficiary.example:pt",
"name": "Example Exchange",
"jurisdiction": "PT"
}
}curl https://sandbox.notabene.id/api/v1/transfers/assess \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"asset": "ETH",
"amount": "0.5",
"destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
}'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/assess', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"asset": "ETH",
"amount": "0.5",
"destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
}),
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.post(
'https://sandbox.notabene.id/api/v1/transfers/assess',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
json={
"asset": "ETH",
"amount": "0.5",
"destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
},
)
answer = None if response.status_code == 204 else response.json(){
"travelRule": "Applies",
"isTravelRule": true,
"presentationDefinitionUrl": "https://pd.notabene.id/ivms101/v2/PT-0.json",
"threshold": { "amount": "0", "currency": "EUR" },
"required": [
"originatorName",
"originatorAccountNumber",
"beneficiaryName",
"beneficiaryAccountNumber",
{
"oneOf": [
"originatorDateAndPlaceOfBirth",
"originatorCustomerIdentification",
"originatorGeographicAddress",
"originatorNationalIdentification"
]
}
],
"beneficiaryVasp": {
"did": "did:web:beneficiary.example:pt",
"name": "Example Exchange",
"jurisdiction": "PT"
}
}Amounts are written in the asset’s whole units, for example 0.5 ETH rather than an amount in wei. For an asset Notabene cannot price, add fiatAmountUsd with your own valuation.
Reading the assessment
The travelRule value in the response from POST /api/v1/transfers/assess tells you what to collect.
| travelRule | What to collect |
|---|---|
Applies | The fields listed in required. A oneOf group is met by any one of its fields. |
Exempt | Nothing. The Travel Rule does not apply to this transfer, for example because the amount is below the threshold. |
Undetermined | Notabene cannot tell whether the Travel Rule applies. Common practice is to send both names and both account numbers, so ask your customer for the beneficiary’s name. |
| travelRule | What to collect |
|---|---|
Applies | The fields listed in required. A oneOf group is met by any one of its fields. |
Exempt | Nothing. The Travel Rule does not apply to this transfer, for example because the amount is below the threshold. |
Undetermined | Notabene cannot tell whether the Travel Rule applies. Common practice is to send both names and both account numbers, so ask your customer for the beneficiary’s name. |
If no institution is found
If the response does not name an institution for the destination address, ask your customer where the funds are going.
- If it is an account at an exchange, let your customer pick the institution from the Notabene network, and pass it as
beneficiary_entity_didwhen you create the transfer. - If it is their own wallet, there is no name to ask for, so collect a proof that the wallet belongs to them (
POST /api/v1/customers/{customer}/wallets/{address}/proofs), as described in Wallets. - If it is someone else’s wallet, ask your customer for the owner’s name.
You can search the network for the institution as your customer types.
curl --get https://sandbox.notabene.id/api/v1/network/entities \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --data-urlencode 'q=Example Exchange'const url = new URL('https://sandbox.notabene.id/api/v1/network/entities'); url.searchParams.set('q', 'Example Exchange'); const response = await fetch(url, { method: 'GET', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', }, }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.get( 'https://sandbox.notabene.id/api/v1/network/entities', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, params={ "q": "Example Exchange" }, ) answer = None if response.status_code == 204 else response.json()
What to replace
Example Exchange: What your customer types
{
"data": [
{
"did": "did:web:beneficiary.example:pt",
"name": "Example Exchange",
"legalName": "Example Exchange, S.A.",
"jurisdiction": "PT",
"status": "receive_respond_send",
"verificationStatus": "verified"
}
]
}curl --get https://sandbox.notabene.id/api/v1/network/entities \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--data-urlencode 'q=Example Exchange'const url = new URL('https://sandbox.notabene.id/api/v1/network/entities');
url.searchParams.set('q', 'Example Exchange');
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
},
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.get(
'https://sandbox.notabene.id/api/v1/network/entities',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
params={
"q": "Example Exchange"
},
)
answer = None if response.status_code == 204 else response.json(){
"data": [
{
"did": "did:web:beneficiary.example:pt",
"name": "Example Exchange",
"legalName": "Example Exchange, S.A.",
"jurisdiction": "PT",
"status": "receive_respond_send",
"verificationStatus": "verified"
}
]
}Results come in pages of up to 100, starting at page 1.
B2. Collect the required fields
Show your customer only the beneficiary fields listed in the assessment, which is usually just the beneficiary’s name. Information about your own customer comes from your records, so there is no need to ask for it again. A oneOf group is satisfied by any one of its fields, so use one you already hold.
Where each field comes from
Each value in required stands for an IVMS-101 field. The presentationDefinitionUrl in the assessment is the full rule set of the jurisdiction that applies, and the list of jurisdictions links every jurisdiction’s rule set with its threshold.
| Value in required | The IVMS-101 field | Where it comes from |
|---|---|---|
originatorName | The originator’s naturalPerson.name, or legalPerson.name for an organization. | Your records. |
originatorAccountNumber | The originator’s accountNumber. | Your records. |
originatorGeographicAddress | The originator’s geographicAddress. | Your records. |
originatorNationalIdentification | The originator’s nationalIdentification. | Your records. |
originatorDateAndPlaceOfBirth | The originator’s dateAndPlaceOfBirth, for a person only. | Your records. |
originatorCustomerIdentification | The originator’s customerIdentification. | Your records. |
beneficiaryName | The beneficiary’s naturalPerson.name, or legalPerson.name for an organization. | Ask your customer for the name of the account holder at the destination. |
beneficiaryAccountNumber | The beneficiary’s accountNumber. | The destination address your customer entered. |
beneficiaryGeographicAddress | The beneficiary’s geographicAddress. | Ask your customer, when it is listed. |
| Value in required | The IVMS-101 field | Where it comes from |
|---|---|---|
originatorName | The originator’s naturalPerson.name, or legalPerson.name for an organization. | Your records. |
originatorAccountNumber | The originator’s accountNumber. | Your records. |
originatorGeographicAddress | The originator’s geographicAddress. | Your records. |
originatorNationalIdentification | The originator’s nationalIdentification. | Your records. |
originatorDateAndPlaceOfBirth | The originator’s dateAndPlaceOfBirth, for a person only. | Your records. |
originatorCustomerIdentification | The originator’s customerIdentification. | Your records. |
beneficiaryName | The beneficiary’s naturalPerson.name, or legalPerson.name for an organization. | Ask your customer for the name of the account holder at the destination. |
beneficiaryAccountNumber | The beneficiary’s accountNumber. | The destination address your customer entered. |
beneficiaryGeographicAddress | The beneficiary’s geographicAddress. | Ask your customer, when it is listed. |
When the destination is your customer’s own wallet, they are also the beneficiary, so take the beneficiary fields from your records and collect a proof of ownership instead.
Then create the transfer
Both approaches end the same way, with your backend creating the transfer from the data you collected. Notabene checks it against the requirements again, applies your policies and sends the transfer to the counterparty institution.
Withdrawals covers this step, and its call and response are in Withdrawals, step 1.
Notabene finds the institution from settlement_address, and if your customer picked the institution themselves, you also pass beneficiary_entity_did. When the destination is your customer’s own wallet, set beneficiary_id to the wallet’s did:pkh and include the proof in beneficiary_proof_of_ownership.
When a call fails
| Answer | What it means |
|---|---|
400 with expected required property amount to be present | The assessment needs the asset and the amount. |
400 with expected required property customer_ref to be present | A delegate token is for one customer. Send their id as customer_ref. |
401 with invalid bearer token | The delegate token has expired. It lasts 1 hour, so request a new one. |
403 with token subject does not match customer | The delegate token belongs to another customer. |
| Answer | What it means |
|---|---|
400 with expected required property amount to be present | The assessment needs the asset and the amount. |
400 with expected required property customer_ref to be present | A delegate token is for one customer. Send their id as customer_ref. |
401 with invalid bearer token | The delegate token has expired. It lasts 1 hour, so request a new one. |
403 with token subject does not match customer | The delegate token belongs to another customer. |
Errors lists every other answer.
Common mistakes
| Mistake | What happens | Instead |
|---|---|---|
| Giving your app your access token | Your app can then do everything your backend can. | Give it a delegate token, which only works for one customer. |
| Asking your customer for their own details | Your customer types again what you already hold. | Take the originator’s fields from your records, and ask only for the beneficiary’s. |
| The amount in the asset’s smallest unit, such as wei | The assessment reads it in whole units, so it may apply the wrong threshold. | Send "0.5" for 0.5 ETH. |
| Mistake | What happens | Instead |
|---|---|---|
| Giving your app your access token | Your app can then do everything your backend can. | Give it a delegate token, which only works for one customer. |
| Asking your customer for their own details | Your customer types again what you already hold. | Take the originator’s fields from your records, and ask only for the beneficiary’s. |
| The amount in the asset’s smallest unit, such as wei | The assessment reads it in whole units, so it may apply the wrong threshold. | Send "0.5" for 0.5 ETH. |
Updated 10 days ago
