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.

Embedded component
Your own UI
Steps
Embedded component
Your own UI

Done by Notabene

Only when needed

← → the step before and after · Esc all steps
Step
Your customer requests a withdrawal
Your customer, in your app

With the destination address, the asset and the amount.

← → the step before and after · Esc all steps
Embedded component · Step A1
A1Get a delegate token
Your backend

Your backend requests one for this customer: A1. Get a delegate token.

← → the step before and after · Esc all steps
Embedded component
Open Withdrawal Assist
Your frontend, then your customer

It asks your customer for what the rules require: A2. Open Withdrawal Assist.

← → the step before and after · Esc all steps
Your own UI · Step B1
B1Assess the transfer
Your backend

Ask Notabene what this transfer requires: B1. Assess the transfer.

← → the step before and after · Esc all steps
Your own UI · Only when needed
If no institution is found
Your app

Ask your customer where the funds are going: If no institution is found.

← → the step before and after · Esc all steps
Your own UI · Step B2
B2Collect the required fields
Your app

Ask only for what the assessment lists: B2. Collect the required fields.

← → the step before and after · Esc all steps
Step
Create the transfer
Your backend

With the data you collected: Then create the transfer.

← → the step before and after · Esc all steps
  1. Your customer requests a withdrawal (Your customer, in your app). With the destination address, the asset and the amount.
  2. 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.
  3. Embedded component: Open Withdrawal Assist (Your frontend, then your customer). It asks your customer for what the rules require: A2. Open Withdrawal Assist.
  4. Your own UI: Assess the transfer (POST /api/v1/transfers/assess, Your backend). Ask Notabene what this transfer requires: B1. Assess the transfer.
  5. 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.
  6. Your own UI: Collect the required fields (Your app). Ask only for what the assessment lists: B2. Collect the required fields.
  7. 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.

API reference
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 DID
  • urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27: The id of your customer
Response201
{
  "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.

ComponentUseReturns
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.

API reference
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
Response200
{
  "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.

travelRuleWhat to collect
AppliesThe fields listed in required. A oneOf group is met by any one of its fields.
ExemptNothing. The Travel Rule does not apply to this transfer, for example because the amount is below the threshold.
UndeterminedNotabene 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.
travelRuleWhat to collect
AppliesThe fields listed in required. A oneOf group is met by any one of its fields.
ExemptNothing. The Travel Rule does not apply to this transfer, for example because the amount is below the threshold.
UndeterminedNotabene 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_did when 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()
{
  "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 requiredThe IVMS-101 fieldWhere it comes from
originatorNameThe originator’s naturalPerson.name, or legalPerson.name for an organization.Your records.
originatorAccountNumberThe originator’s accountNumber.Your records.
originatorGeographicAddressThe originator’s geographicAddress.Your records.
originatorNationalIdentificationThe originator’s nationalIdentification.Your records.
originatorDateAndPlaceOfBirthThe originator’s dateAndPlaceOfBirth, for a person only.Your records.
originatorCustomerIdentificationThe originator’s customerIdentification.Your records.
beneficiaryNameThe beneficiary’s naturalPerson.name, or legalPerson.name for an organization.Ask your customer for the name of the account holder at the destination.
beneficiaryAccountNumberThe beneficiary’s accountNumber.The destination address your customer entered.
beneficiaryGeographicAddressThe beneficiary’s geographicAddress.Ask your customer, when it is listed.
Value in requiredThe IVMS-101 fieldWhere it comes from
originatorNameThe originator’s naturalPerson.name, or legalPerson.name for an organization.Your records.
originatorAccountNumberThe originator’s accountNumber.Your records.
originatorGeographicAddressThe originator’s geographicAddress.Your records.
originatorNationalIdentificationThe originator’s nationalIdentification.Your records.
originatorDateAndPlaceOfBirthThe originator’s dateAndPlaceOfBirth, for a person only.Your records.
originatorCustomerIdentificationThe originator’s customerIdentification.Your records.
beneficiaryNameThe beneficiary’s naturalPerson.name, or legalPerson.name for an organization.Ask your customer for the name of the account holder at the destination.
beneficiaryAccountNumberThe beneficiary’s accountNumber.The destination address your customer entered.
beneficiaryGeographicAddressThe 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

AnswerWhat it means
400 with expected required property amount to be presentThe assessment needs the asset and the amount.
400 with expected required property customer_ref to be presentA delegate token is for one customer. Send their id as customer_ref.
401 with invalid bearer tokenThe delegate token has expired. It lasts 1 hour, so request a new one.
403 with token subject does not match customerThe delegate token belongs to another customer.
AnswerWhat it means
400 with expected required property amount to be presentThe assessment needs the asset and the amount.
400 with expected required property customer_ref to be presentA delegate token is for one customer. Send their id as customer_ref.
401 with invalid bearer tokenThe delegate token has expired. It lasts 1 hour, so request a new one.
403 with token subject does not match customerThe delegate token belongs to another customer.

Errors lists every other answer.

Common mistakes

MistakeWhat happensInstead
Giving your app your access tokenYour 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 detailsYour 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 weiThe assessment reads it in whole units, so it may apply the wrong threshold.Send "0.5" for 0.5 ETH.
MistakeWhat happensInstead
Giving your app your access tokenYour 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 detailsYour 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 weiThe assessment reads it in whole units, so it may apply the wrong threshold.Send "0.5" for 0.5 ETH.

Did this page help you?