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 the Embedded component, which works out the requirements and shows the right fields, 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.
Before you start
- An access token, as described in Authentication. The examples use the sandbox base URL.
- 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
The component works out the requirements and shows your customer only 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 only lets the component act on behalf of this one customer, while the access token your backend uses is covered in Authentication.
A2. Open the Embedded component
Your frontend opens the component with the delegate token and the withdrawal details. When your customer is done, the component returns the collected data to your app (the event and the data format (placeholder)). Its options and events are described in Embedded component (placeholder).
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. |
How it works
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 the Embedded component.
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.
Updated about 6 hours ago
