Moving from V1 to v2.1
V1 and v2.1 are separate platforms, so moving means a new account and a new integration rather than an upgrade. Your v2.1 account has its own DID, API credentials and webhook endpoint, and your backend replaces each V1 call with its v2.1 counterpart.
Only the access token request keeps its shape. Most flows also take fewer calls, because your policies authorize every transfer that passes them, where V1 often waited for your backend to approve, confirm or accept it.
Main differences
| Topic | V1 | v2.1 |
|---|---|---|
| Base URL | https:/, and https:/ for testing. | https:/ in sandbox, and your region’s address in production, as Authentication lists. |
| Token audience | https:/ | The environment’s address, such as https:/. |
| Your DID | Your V1 DID, such as a did:ethr. | A did:web created with your account, as in Create your account. |
| Your institution on a call | vaspDID, originatorVASPdid or beneficiaryVASPdid. | Taken from your access token, so you leave it out. |
| Amounts | transactionAmount in the asset’s smallest unit, such as wei. | amount in whole units, such as "0.5" for 0.5 ETH. |
| Addresses | A bare address, with the asset naming the chain. | CAIP-10, such as eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed. Transfers also take a bare address. |
| Your customers | Sent in full on every transfer, in originatorPersons or beneficiaryPersons. | Registered once, then named on each transfer by your own id. |
| IVMS-101 names | name as an array, with nameIdentifierType. | name as an object, with naturalPersonNameIdentifierType. |
| Decisions | txApprove, txAccept and txDecline, unless rules made them. | Your policies approve, flag or reject. You call only for a flagged transfer. |
| Personal data you encrypt | encrypted and pii on the transfer. | ivms_encrypted, one JWE per person, as Encryption describes. |
| Webhooks | Registered through the API. | Added in the dashboard, under Developers › Webhooks, with signed deliveries. |
| Topic | V1 | v2.1 |
|---|---|---|
| Base URL | https://api.notabene.id, and https://api.notabene.dev for testing. | https://sandbox.notabene.id/api/v1 in sandbox, and your region’s address in production, as Authentication lists. |
| Token audience | https://api.notabene.id | The environment’s address, such as https://sandbox.notabene.id. |
| Your DID | Your V1 DID, such as a did:ethr. | A did:web created with your account, as in Create your account. |
| Your institution on a call | vaspDID, originatorVASPdid or beneficiaryVASPdid. | Taken from your access token, so you leave it out. |
| Amounts | transactionAmount in the asset’s smallest unit, such as wei. | amount in whole units, such as "0.5" for 0.5 ETH. |
| Addresses | A bare address, with the asset naming the chain. | CAIP-10, such as eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed. Transfers also take a bare address. |
| Your customers | Sent in full on every transfer, in originatorPersons or beneficiaryPersons. | Registered once, then named on each transfer by your own id. |
| IVMS-101 names | name as an array, with nameIdentifierType. | name as an object, with naturalPersonNameIdentifierType. |
| Decisions | txApprove, txAccept and txDecline, unless rules made them. | Your policies approve, flag or reject. You call only for a flagged transfer. |
| Personal data you encrypt | encrypted and pii on the transfer. | ivms_encrypted, one JWE per person, as Encryption describes. |
| Webhooks | Registered through the API. | Added in the dashboard, under Developers › Webhooks, with signed deliveries. |
Setup
Set up a sandbox account first, and repeat the setup in production once your integration works there. Sandbox and production are separate accounts, each with its own credentials, webhook endpoint, customers and addresses.
Publish your DID document
In production. In sandbox, Notabene publishes it.
Page
Set your policies
In the dashboard, under Authorization Rules › Policies. A new account flags every transfer until you change its general default.
Page
Choose your requirements preset
In the dashboard, under Authorization Rules › Requirements.
Page
| Step | Where | Page |
|---|---|---|
| Create your account and have it activated | In the dashboard. | Create your account |
| Create an API credential | In the dashboard, under Developers › API Credentials. | Authentication |
| Publish your DID document | In production. In sandbox, Notabene publishes it. | Host your DID document |
| Set your policies | In the dashboard, under Authorization Rules › Policies. A new account flags every transfer until you change its general default. | Policies |
| Choose your requirements preset | In the dashboard, under Authorization Rules › Requirements. | Requirements |
| Add your webhook endpoint | In the dashboard, under Developers › Webhooks. | Webhooks |
| Register your customers and their deposit addresses | Through the API, as below. | Customers |
Counterparty institutions still on V1 or V2 go on exchanging Travel Rule data with you once you are on v2.1.
Switching over
Outgoing transfers move in one step. From the day your v2.1 account is live in production, every withdrawal goes from it, and your V1 account sends no new ones.
Incoming transfers can still reach your V1 account for a while, because some counterparties keep sending to it. Notabene hides your V1 account from the network and removes its addresses, so counterparties no longer find it, while you can still sign in and handle what arrives there.
Once counterparties have stopped sending to your V1 account, Notabene moves your V1 data into a read-only copy and deletes the V1 account. Where the read-only copy of your V1 data is kept is still to be confirmed. (placeholder)
Calls
Each V1 call has a v2.1 counterpart, or none where your policies now do the work. These are the calls most integrations make, in the order a transfer meets them.
| V1 | v2.1 | What changes |
|---|---|---|
| The access token request | The same request | Your v2.1 client_id and client_secret, and the v2.1 audience. |
customerToken | POST/ | It covers one customer you registered, and lasts 1 hour. |
txValidateFull | POST/ | It takes the asset, amount and destination, and answers what to collect. It is optional. |
address-ownerships | POST/ | The address goes in the body, and the answer names the institution that holds it. |
tfSimpleVASPs | GET/ | Search by name with q. |
txCreate | POST/ | It names your customer by id, with the amount in whole units. |
txUpdate with txHash | POST/ | The hash goes in settlement_id. |
txConfirm | POST/, or confirmed on the address requirement | A registered address needs no answer. |
txReject | rejected on the address requirement | It rejects the transfer. |
registerCustomer | POST/, then POST/ | The customer is registered first, and each address then names the customer’s id. |
txNotify | GET/, then POST/ | You look the deposit up first, and a transfer you create asks the sender for its data. |
txUpdate to complete a deposit | POST/, then PATCH/ | The sending institution goes on the create, and a sender your customer declares goes on the transfer afterwards. |
| V1 | v2.1 | What changes |
|---|---|---|
| The access token request | The same request | Your v2.1 client_id and client_secret, and the v2.1 audience. |
customerToken | POST /api/v1/entities/{entityDID}/delegate-token | It covers one customer you registered, and lasts 1 hour. |
txValidateFull | POST /api/v1/transfers/assess | It takes the asset, amount and destination, and answers what to collect. It is optional. |
address-ownerships | POST /api/v1/wallet-ownerships/discover | The address goes in the body, and the answer names the institution that holds it. |
tfSimpleVASPs | GET /api/v1/network/entities | Search by name with q. |
txCreate | POST /api/v1/transfers | It names your customer by id, with the amount in whole units. |
txUpdate with txHash | POST /api/v1/transfers/{id}/settle | The hash goes in settlement_id. |
txConfirm | POST /api/v1/wallets, or confirmed on the address requirement | A registered address needs no answer. |
txReject | rejected on the address requirement | It rejects the transfer. |
registerCustomer | POST /api/v1/customers, then POST /api/v1/wallets | The customer is registered first, and each address then names the customer’s id. |
txNotify | GET /api/v1/transfers/match, then POST /api/v1/transfers | You look the deposit up first, and a transfer you create asks the sender for its data. |
txUpdate to complete a deposit | POST /api/v1/transfers, then PATCH /api/v1/transfers/{id}/parties/originator | The sending institution goes on the create, and a sender your customer declares goes on the transfer afterwards. |
Some integrations also make the calls below, for decisions their backend took itself or to read transfers back.
| V1 | v2.1 | What changes |
|---|---|---|
txApprove, txAccept, txDecline | No call, unless your policies flag the transfer | A flagged transfer takes force-authorize or reject. |
txCancel | POST/ | Only before settlement, and only by the institution that created the transfer. |
txRedirect | POST/ | Your institution on the transfer is replaced by the one that should receive it. |
txInfo, txList | GET/, GET/ | Your institution comes from the token. |
/ | The dashboard, under Developers › Webhooks | Four events, signed. |
| V1 | v2.1 | What changes |
|---|---|---|
txApprove, txAccept, txDecline | No call, unless your policies flag the transfer | A flagged transfer takes force-authorize or reject. |
txCancel | POST /api/v1/transfers/{id}/cancel | Only before settlement, and only by the institution that created the transfer. |
txRedirect | POST /api/v1/transfers/{id}/agents/replace | Your institution on the transfer is replaced by the one that should receive it. |
txInfo, txList | GET /api/v1/transfers/{id}, GET /api/v1/transfers | Your institution comes from the token. |
/webhook | The dashboard, under Developers › Webhooks | Four events, signed. |
Access token
The request is the same, with the credential from your v2.1 account and the audience of its environment. Every call then sends the token as Authorization: Bearer, as in V1.
V1Access token
POST https://auth.notabene.id/oauth/token
{
"client_id": "YOUR_V1_CLIENT_ID",
"client_secret": "YOUR_V1_CLIENT_SECRET",
"grant_type": "client_credentials",
"audience": "https://api.notabene.dev"
}
v2.1Access token
POST https://auth.notabene.id/oauth/token
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"grant_type": "client_credentials",
"audience": "https://sandbox.notabene.id"
}
POST https://auth.notabene.id/oauth/token
{
"client_id": "YOUR_V1_CLIENT_ID",
"client_secret": "YOUR_V1_CLIENT_SECRET",
"grant_type": "client_credentials",
"audience": "https://api.notabene.dev"
}POST https://auth.notabene.id/oauth/token
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"grant_type": "client_credentials",
"audience": "https://sandbox.notabene.id"
}In production, the audience is your region’s address, which Authentication lists.
Tokens for your customer’s app
V1 gave your frontend a customer token through customerToken. In v2.1 your backend requests a delegate token for one customer you registered, with your own DID in the path and the customer’s id as customer_ref.
V1customerToken
POST https://api.notabene.dev/auth/customerToken
{
"vaspDID": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"customerRef": "customer-123"
}
v2.1Get a delegate token
POST /api/v1/entities/did:web:yourexchange.example:pt/delegate-token
{
"customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}
{
"access_token": "eyJhbGciOiJFZERTQSIs",
"token_type": "Bearer",
"expires_in": 3600,
"jti": "c03044be-8e0b-4bbe-9d2c-c7b28ac3aa5f"
}
POST https://api.notabene.dev/auth/customerToken
{
"vaspDID": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"customerRef": "customer-123"
}POST /api/v1/entities/did:web:yourexchange.example:pt/delegate-token
{
"customer_ref": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}{
"access_token": "eyJhbGciOiJFZERTQSIs",
"token_type": "Bearer",
"expires_in": 3600,
"jti": "c03044be-8e0b-4bbe-9d2c-c7b28ac3aa5f"
}The customer must be registered first, so register customers before your app asks for a token. A delegate token works only on the routes under /api/v1/customers/{customer}/, which Authentication lists, and the embedded components in Data collection use it the same way.
Customers and addresses
In V1, your address book tied an address to a customerRef of your own, and each transfer carried your customer’s data again. In v2.1 you register the customer once with that data, then register each address against the customer’s id.
V1registerCustomer
POST https://api.notabene.dev/address/registerCustomer
{
"vaspDID": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"asset": "ETH",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"customerRef": "customer-123"
}
v2.1Register the customer, then the address
POST /api/v1/customers
{
"id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27",
"type": "Individual",
"jurisdiction": "PT",
"profile": {
"ivms": {
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"naturalPersonNameIdentifierType": "LEGL"
}
]
}
},
"accountNumber": [
"acc-ana-001"
]
}
}
}
POST /api/v1/wallets
{
"address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}
POST https://api.notabene.dev/address/registerCustomer
{
"vaspDID": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"asset": "ETH",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"customerRef": "customer-123"
}POST /api/v1/customers
{
"id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27",
"type": "Individual",
"jurisdiction": "PT",
"profile": {
"ivms": {
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"naturalPersonNameIdentifierType": "LEGL"
}
]
}
},
"accountNumber": [
"acc-ana-001"
]
}
}
}POST /api/v1/wallets
{
"address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}A bare address on POST /api/v1/wallets answers 400, so send it as CAIP-10, which names the chain where V1 used asset. Leave out customer_id for an omnibus wallet, such as a hot wallet. Customers and Wallets list every field.
Checks before a withdrawal
V1 validated a complete transfer, personal data included. In v2.1 the assessment takes only the asset, the amount and the destination. It answers whether the Travel Rule applies, which data to collect and which institution holds the address.
V1txValidateFull
POST https://api.notabene.dev/tx/validate/full
{
"transactionAsset": "ETH",
"transactionAmount": "500000000000000000",
"originatorVASPdid": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"transactionBlockchainInfo": {
"destination": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
},
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"acc-ana-001"
]
},
"beneficiary": {
"beneficiaryPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Smith",
"secondaryIdentifier": "Alice",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
]
}
}
v2.1Assess the transfer
POST /api/v1/transfers/assess
{
"asset": "ETH",
"amount": "0.5",
"destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
}
{
"travelRule": "Applies",
"isTravelRule": true,
"required": [
"originatorName",
"originatorAccountNumber",
"beneficiaryName",
"beneficiaryAccountNumber"
],
"beneficiaryVasp": {
"did": "did:web:beneficiary.example:pt",
"name": "Example Exchange",
"jurisdiction": "PT"
}
}
POST https://api.notabene.dev/tx/validate/full
{
"transactionAsset": "ETH",
"transactionAmount": "500000000000000000",
"originatorVASPdid": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"transactionBlockchainInfo": {
"destination": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
},
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"acc-ana-001"
]
},
"beneficiary": {
"beneficiaryPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Smith",
"secondaryIdentifier": "Alice",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
]
}
}POST /api/v1/transfers/assess
{
"asset": "ETH",
"amount": "0.5",
"destinationAddress": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
}{
"travelRule": "Applies",
"isTravelRule": true,
"required": [
"originatorName",
"originatorAccountNumber",
"beneficiaryName",
"beneficiaryAccountNumber"
],
"beneficiaryVasp": {
"did": "did:web:beneficiary.example:pt",
"name": "Example Exchange",
"jurisdiction": "PT"
}
}The assessment is optional, because creating the transfer runs the same checks. Data collection shows how to read the answer, or how an embedded component collects the data for you.
Finding the counterparty institution
V1 looked up who held an address with address-ownerships, and searched the network with tfSimpleVASPs. In v2.1 discover takes the address in its body, and the network search takes the name in q.
V1address-ownerships, tfSimpleVASPs
GET https://api.notabene.dev/v1/addresses/address-ownerships/0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed?vasp_did=did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695&asset=ETH
GET https://api.notabene.dev/tf/simple/vasps?q=Example%20Exchange
v2.1Discover the owner, or search the network
POST /api/v1/wallet-ownerships/discover
{
"address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
}
{
"source": "AddressHash",
"owner": "did:web:beneficiary.example:pt",
"custodian": "did:web:beneficiary.example:pt"
}
GET /api/v1/network/entities?q=Example Exchange
GET https://api.notabene.dev/v1/addresses/address-ownerships/0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed?vasp_did=did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695&asset=ETHGET https://api.notabene.dev/tf/simple/vasps?q=Example%20ExchangePOST /api/v1/wallet-ownerships/discover
{
"address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
}{
"source": "AddressHash",
"owner": "did:web:beneficiary.example:pt",
"custodian": "did:web:beneficiary.example:pt"
}GET /api/v1/network/entities?q=Example ExchangeKeep the owner, which is the institution holding the address. An address no institution registered answers 404 with address owner not found. Creating the transfer runs the same lookup, so you need these calls only to show the answer before the transfer exists.
Withdrawals
The transfer names your registered customer by id and carries only the beneficiary’s data. Notabene finds the counterparty institution from the address, so you name it only when your customer picked it.
V1txCreate
POST https://api.notabene.dev/tx/create
{
"transactionRef": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"transactionAsset": "ETH",
"transactionAmount": "500000000000000000",
"originatorVASPdid": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"beneficiaryVASPdid": "did:ethr:0xb0e2c1d4a3f5e6d7c8b9a0f1e2d3c4b5a6978877",
"transactionBlockchainInfo": {
"origin": "0xa40dfee99e1c85dc97fdc594b16a460717838703",
"destination": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
},
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"acc-ana-001"
]
},
"beneficiary": {
"beneficiaryPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Smith",
"secondaryIdentifier": "Alice",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
]
}
}
v2.1Create the transfer
POST /api/v1/transfers
{
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"asset": "ETH",
"amount": "0.5",
"settlement_address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"originator_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27",
"beneficiary_id": "urn:uuid:e7d14b39-8a2c-4f05-b6e1-2c9f7a3d5b18",
"beneficiary": {
"ivms": {
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "Smith",
"secondaryIdentifier": "Alice",
"naturalPersonNameIdentifierType": "LEGL"
}
]
}
}
}
}
}
{
"transferId": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Active"
}
POST https://api.notabene.dev/tx/create
{
"transactionRef": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"transactionAsset": "ETH",
"transactionAmount": "500000000000000000",
"originatorVASPdid": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"beneficiaryVASPdid": "did:ethr:0xb0e2c1d4a3f5e6d7c8b9a0f1e2d3c4b5a6978877",
"transactionBlockchainInfo": {
"origin": "0xa40dfee99e1c85dc97fdc594b16a460717838703",
"destination": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
},
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"acc-ana-001"
]
},
"beneficiary": {
"beneficiaryPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Smith",
"secondaryIdentifier": "Alice",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
]
}
}POST /api/v1/transfers
{
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"asset": "ETH",
"amount": "0.5",
"settlement_address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"originator_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27",
"beneficiary_id": "urn:uuid:e7d14b39-8a2c-4f05-b6e1-2c9f7a3d5b18",
"beneficiary": {
"ivms": {
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "Smith",
"secondaryIdentifier": "Alice",
"naturalPersonNameIdentifierType": "LEGL"
}
]
}
}
}
}
}{
"transferId": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Active"
}Keep the transferId from the response, because every later call takes it in its path.
| V1 field | v2.1 field | What changes |
|---|---|---|
transactionRef | ref Required string, UUID | Required. Notabene never creates two transfers with the same ref, so a retry is safe. |
transactionAsset | asset Required string | A symbol such as ETH, or a CAIP-19 identifier. Assets lists them. |
transactionAmount | amount Required string, decimal | Whole units, so "0.5" where V1 took 500000000000000000 wei. |
transactionAssetDecimals | None | Not sent, because the amount is in whole units. |
transactionBlockchainInfo.destination | settlement_address Required string | CAIP-10 or a bare address. Notabene finds the counterparty institution from it. |
transactionBlockchainInfo.origin | source_address Optional string | The address the funds leave from. |
originatorVASPdid | None | Your institution comes from your access token. |
beneficiaryVASPdid | beneficiary_entity_did Optional string, DID | Only when your customer picked the counterparty institution themselves. |
originator.originatorPersons | originator_id Required string | The id of the customer you registered. |
beneficiary.beneficiaryPersons | beneficiary_id Required and beneficiary.ivms | An id of your choosing, and the beneficiary’s name as IVMS-101 when the Travel Rule asks for it. |
beneficiaryProof | beneficiary_proof_of_ownership Optional | The proof for your customer’s own self-hosted wallet, as Wallets describes. |
| V1 field | v2.1 field | What changes |
|---|---|---|
transactionRef | ref (required), string, UUID | Required. Notabene never creates two transfers with the same ref, so a retry is safe. |
transactionAsset | asset (required), string | A symbol such as ETH, or a CAIP-19 identifier. Assets lists them. |
transactionAmount | amount (required), string, decimal | Whole units, so "0.5" where V1 took 500000000000000000 wei. |
transactionAssetDecimals | None | Not sent, because the amount is in whole units. |
transactionBlockchainInfo.destination | settlement_address (required), string | CAIP-10 or a bare address. Notabene finds the counterparty institution from it. |
transactionBlockchainInfo.origin | source_address (optional), string | The address the funds leave from. |
originatorVASPdid | None | Your institution comes from your access token. |
beneficiaryVASPdid | beneficiary_entity_did (optional), string, DID | Only when your customer picked the counterparty institution themselves. |
originator.originatorPersons | originator_id (required), string | The id of the customer you registered. |
beneficiary.beneficiaryPersons | beneficiary_id (required) and beneficiary.ivms | An id of your choosing, and the beneficiary’s name as IVMS-101 when the Travel Rule asks for it. |
beneficiaryProof | beneficiary_proof_of_ownership (optional) | The proof for your customer’s own self-hosted wallet, as Wallets describes. |
Withdrawals has the whole flow, with its errors.
Approving and declining
In V1, your backend approved an outgoing transfer with txApprove, and accepted or declined an incoming one with txAccept or txDecline. In v2.1 your policies approve, flag or reject each transfer within seconds, and there is no call when they approve it.
A flagged transfer waits for a person on your team, who decides in the dashboard Inbox or through the API. Approving overrides your policies’ verdict, so it needs a reason, which stays on the transfer’s timeline.
V1txApprove, txAccept, txDecline
POST https://api.notabene.dev/tx/approve?id=V1_TRANSFER_ID
POST https://api.notabene.dev/tx/decline
{
"id": "V1_TRANSFER_ID",
"reason": "Destination not allowed"
}
v2.1Decide on a flagged transfer
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize
{
"reason": "Reviewed by compliance"
}
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject
{
"reason": "Destination not allowed"
}
POST https://api.notabene.dev/tx/approve?id=V1_TRANSFER_IDPOST https://api.notabene.dev/tx/decline
{
"id": "V1_TRANSFER_ID",
"reason": "Destination not allowed"
}POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize
{
"reason": "Reviewed by compliance"
}POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject
{
"reason": "Destination not allowed"
}Approving answers 204, and rejecting answers 200 with the transfer, now Rejected. The same two calls work for withdrawals and deposits.
Canceling a withdrawal
When your customer cancels a withdrawal before it is settled, POST /api/v1/transfers/{id}/cancel replaces txCancel. Only the institution that created the transfer can cancel it, and the reason is shared with the counterparty.
V1txCancel
POST https://api.notabene.dev/tx/cancel
{
"id": "V1_TRANSFER_ID",
"reason": "Canceled by the customer"
}
v2.1Cancel the transfer
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/cancel
{
"reason": "Canceled by the customer"
}
POST https://api.notabene.dev/tx/cancel
{
"id": "V1_TRANSFER_ID",
"reason": "Canceled by the customer"
}POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/cancel
{
"reason": "Canceled by the customer"
}The call answers 200 with the transfer, now Canceled, which Statuses describes.
Transaction hash
In V1 you added the hash to the transfer with txUpdate. In v2.1 it has its own call, made once every institution has authorized the transfer, which transfer.agent_status_changed.v3 tells you.
V1txUpdate
POST https://api.notabene.dev/tx/update
{
"id": "V1_TRANSFER_ID",
"txHash": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
}
v2.1Settle the transfer
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle
{
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
}
{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Settled",
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"direction": "Outgoing"
}
POST https://api.notabene.dev/tx/update
{
"id": "V1_TRANSFER_ID",
"txHash": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
}POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle
{
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
}{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Settled",
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"direction": "Outgoing"
}Recording the same hash again answers 200, while a different hash answers 409.
Address confirmation
In V1, every incoming transfer waited for txConfirm or txReject. In v2.1 Notabene confirms an address you registered, so with your deposit addresses registered you rarely answer at all.
When Notabene cannot match the address, requirements.pending.v3 asks you about it. Register the address, confirm it for this transfer only, or reject it.
V1txConfirm, txReject
POST https://api.notabene.dev/tx/confirm?id=V1_TRANSFER_ID
POST https://api.notabene.dev/tx/reject
{
"id": "V1_TRANSFER_ID",
"reason": "Address not ours"
}
v2.1Answer the address requirement
POST /api/v1/wallets
{
"address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/wallet-address-confirmation/resolve
{
"outcome": "confirmed"
}
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/wallet-address-confirmation/resolve
{
"outcome": "rejected"
}
POST https://api.notabene.dev/tx/confirm?id=V1_TRANSFER_IDPOST https://api.notabene.dev/tx/reject
{
"id": "V1_TRANSFER_ID",
"reason": "Address not ours"
}POST /api/v1/wallets
{
"address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/wallet-address-confirmation/resolve
{
"outcome": "confirmed"
}POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/wallet-address-confirmation/resolve
{
"outcome": "rejected"
}Registering the address answers every waiting transfer to it, and later ones too. Rejecting it rejects the transfer, and the counterparty stops treating the address as yours. Deposits, Travel Rule data first has the whole deposit flow.
Deposits without Travel Rule data
V1 used txNotify to tell the sender about a deposit that came without its Travel Rule data. In v2.1 the data usually arrives before the funds, so you first look the deposit up by its hash.
When nothing matches, you create the transfer as the beneficiary, with the hash. Naming the sending institution asks it for its data, and POST /api/v1/wallet-ownerships/discover finds that institution from the address the funds came from.
V1txNotify
POST https://api.notabene.dev/tx/notify
{
"txHash": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"transactionAmount": "500000000000000000",
"transactionAsset": "ETH",
"beneficiaryVASPdid": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"destination": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"origin": "0xa40dfee99e1c85dc97fdc594b16a460717838703",
"customerRef": "customer-123"
}
v2.1Look the deposit up, then ask for the data
GET /api/v1/transfers/match?settlement_id=0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d&settlement_address=eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
POST /api/v1/transfers
{
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"asset": "ETH",
"amount": "0.5",
"source_address": "eip155:1:0xa40dfee99e1c85dc97fdc594b16a460717838703",
"settlement_address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"originator_entity_did": "did:web:originator.example:pt",
"beneficiary_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}
POST https://api.notabene.dev/tx/notify
{
"txHash": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"transactionAmount": "500000000000000000",
"transactionAsset": "ETH",
"beneficiaryVASPdid": "did:ethr:0x6c3b1f6a3e2d4c5b8a9e0f1d2c3b4a5968778695",
"destination": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"origin": "0xa40dfee99e1c85dc97fdc594b16a460717838703",
"customerRef": "customer-123"
}GET /api/v1/transfers/match?settlement_id=0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d&settlement_address=eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAedPOST /api/v1/transfers
{
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"asset": "ETH",
"amount": "0.5",
"source_address": "eip155:1:0xa40dfee99e1c85dc97fdc594b16a460717838703",
"settlement_address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"originator_entity_did": "did:web:originator.example:pt",
"beneficiary_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}When the lookup finds the transfer, credit your customer once it is Settled.
Completing a deposit
In V1 you completed a deposit’s data with txUpdate, naming the sending institution, marking the wallet as self-hosted or adding the sender yourself. In v2.1 the sending institution goes on the create above, so only the last two cases take a call of their own.
For a deposit from a self-hosted wallet, or one whose sender no institution claims, create the transfer with your own DID as the beneficiary’s institution. Then add the sender your customer declared, using the originator.id that GET /api/v1/transfers/{id} shows.
V1txUpdate
POST https://api.notabene.dev/tx/update
{
"id": "V1_TRANSFER_ID",
"originatorVASPdid": "did:ethr:0xb0e2c1d4a3f5e6d7c8b9a0f1e2d3c4b5a6978877"
}
POST https://api.notabene.dev/tx/update
{
"id": "V1_TRANSFER_ID",
"isNonCustodial": true,
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"0xa40dfee99e1c85dc97fdc594b16a460717838703"
]
}
}
v2.1Create the transfer, then add the sender
POST /api/v1/transfers
{
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"asset": "ETH",
"amount": "0.5",
"source_address": "eip155:1:0xa40dfee99e1c85dc97fdc594b16a460717838703",
"settlement_address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"beneficiary_entity_did": "did:web:yourexchange.example:pt",
"beneficiary_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}
PATCH /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/parties/originator
{
"party": {
"id": "urn:uuid:9a41c7e2-6b3d-4f58-8e1a-2d7c5b9f0e36",
"ivms": {
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"naturalPersonNameIdentifierType": "LEGL"
}
]
},
"dateAndPlaceOfBirth": {
"dateOfBirth": "1988-03-02",
"placeOfBirth": "Porto"
}
}
}
},
"reason": "Declared by our customer"
}
POST https://api.notabene.dev/tx/update
{
"id": "V1_TRANSFER_ID",
"originatorVASPdid": "did:ethr:0xb0e2c1d4a3f5e6d7c8b9a0f1e2d3c4b5a6978877"
}POST https://api.notabene.dev/tx/update
{
"id": "V1_TRANSFER_ID",
"isNonCustodial": true,
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"name": [
{
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"nameIdentifierType": "LEGL"
}
]
}
]
}
}
],
"accountNumber": [
"0xa40dfee99e1c85dc97fdc594b16a460717838703"
]
}
}POST /api/v1/transfers
{
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"asset": "ETH",
"amount": "0.5",
"source_address": "eip155:1:0xa40dfee99e1c85dc97fdc594b16a460717838703",
"settlement_address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"settlement_id": "0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"beneficiary_entity_did": "did:web:yourexchange.example:pt",
"beneficiary_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
}PATCH /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/parties/originator
{
"party": {
"id": "urn:uuid:9a41c7e2-6b3d-4f58-8e1a-2d7c5b9f0e36",
"ivms": {
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "Silva",
"secondaryIdentifier": "Ana",
"naturalPersonNameIdentifierType": "LEGL"
}
]
},
"dateAndPlaceOfBirth": {
"dateOfBirth": "1988-03-02",
"placeOfBirth": "Porto"
}
}
}
},
"reason": "Declared by our customer"
}Adding the sender answers 204, and the one call puts everything you collected about the sender on the transfer. If you know only the name, send name in place of ivms. Deposits, funds first has the whole flow, with its errors.
Redirecting a transfer
When your group holds institutions in several jurisdictions, V1 routed an incoming transfer from one to another with txRedirect. In v2.1 your institution on the transfer replaces itself with the institution that should handle it.
V1txRedirect
POST https://api.notabene.dev/tx/redirect?id=V1_TRANSFER_ID&to=did:ethr:0x2f4e6a8c0b1d3f5a7c9e1b3d5f7a9c0e2b4d6f81
v2.1Replace your institution on the transfer
POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/agents/replace
{
"original": "did:web:yourexchange.example:pt",
"replacement": {
"id": "did:web:yourexchange.example:fr"
}
}
POST https://api.notabene.dev/tx/redirect?id=V1_TRANSFER_ID&to=did:ethr:0x2f4e6a8c0b1d3f5a7c9e1b3d5f7a9c0e2b4d6f81POST /api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/agents/replace
{
"original": "did:web:yourexchange.example:pt",
"replacement": {
"id": "did:web:yourexchange.example:fr"
}
}The replacement keeps your institution’s role and the customer it acts for, and the call answers 200 with the transfer. An institution can replace only itself, and once replaced it can take no further action on the transfer.
Reading transfers
GET /api/v1/transfers/{id} replaces txInfo, and GET /api/v1/transfers replaces txList, without the vaspDID that V1 asked for. A transfer has its own status, and each institution on it has one too, as Statuses explains.
Webhooks
Add your endpoint in the dashboard, under Developers › Webhooks, rather than through the API. It receives four events: transfer.created.v3, transfer.status_changed.v3, transfer.agent_status_changed.v3 and requirements.pending.v3.
Each delivery is signed, so verify it with your endpoint’s signing secret. Deliveries can repeat or arrive out of order, as Webhooks describes.
Common mistakes
| Mistake | What happens | Instead |
|---|---|---|
| The amount in the smallest unit, as in V1 | 500000000000000000 is read as that many ETH. | Send "0.5" for 0.5 ETH. |
| A bare address when you register it | POST/ answers 400. | Send it as CAIP-10. |
| IVMS-101 names in the V1 shape | The request fails validation. | Send name as an object, with naturalPersonNameIdentifierType. |
An inline beneficiary without beneficiary_id | 400 with party profile provided without party ID. | Send beneficiary_id with every inline beneficiary. |
| Your V1 credential or audience | The token request fails. | Use the credential and audience of your v2.1 account. |
| Mistake | What happens | Instead |
|---|---|---|
| The amount in the smallest unit, as in V1 | 500000000000000000 is read as that many ETH. | Send "0.5" for 0.5 ETH. |
| A bare address when you register it | POST /api/v1/wallets answers 400. | Send it as CAIP-10. |
| IVMS-101 names in the V1 shape | The request fails validation. | Send name as an object, with naturalPersonNameIdentifierType. |
An inline beneficiary without beneficiary_id | 400 with party profile provided without party ID. | Send beneficiary_id with every inline beneficiary. |
| Your V1 credential or audience | The token request fails. | Use the credential and audience of your v2.1 account. |
Go live lists the checks to make in sandbox before you move production traffic.
Updated 3 days ago
