Withdrawals
To make a withdrawal, your backend creates a transfer and Notabene takes care of the Travel Rule, from finding the counterparty institution to sending its Travel Rule data. When the transfer is ready to settle, you send the funds and record the transaction hash.
The main path takes two calls: one to create the transfer and one to record the hash.
Before you start
- An access token, as described in Authentication. The examples use the sandbox base URL.
- Your customer, who sends the funds, registered with Notabene, as described in Customers. The transfer refers to them by their
id. - A webhook endpoint, as described in Webhooks, so that Notabene can tell you when the transfer is ready to settle.
A new account flags every transfer for review until you change its general default, which is set in Policies.
1. Create the transfer
curl https://sandbox.notabene.id/api/v1/transfers \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "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" } ] } } } } }'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers', { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ "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" } ] } } } } }), }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.post( 'https://sandbox.notabene.id/api/v1/transfers', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, json={ "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" } ] } } } } }, ) answer = None if response.status_code == 204 else response.json()
What to replace
0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40: A new UUID of yours for each transfereip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed: The destination address your customer enteredurn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27: The id of your customer who sends the fundsurn:uuid:e7d14b39-8a2c-4f05-b6e1-2c9f7a3d5b18: An id of your choosing for the beneficiarySmith: The beneficiary’s last nameAlice: The beneficiary’s first names
{
"transferId": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Active"
}curl https://sandbox.notabene.id/api/v1/transfers \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"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"
}
]
}
}
}
}
}'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"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"
}
]
}
}
}
}
}),
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.post(
'https://sandbox.notabene.id/api/v1/transfers',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
json={
"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"
}
]
}
}
}
}
},
)
answer = None if response.status_code == 204 else response.json(){
"transferId": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Active"
}Keep the transferId from the response: every later call takes it in its path, and events and other responses call it id.
| Field | What to send |
|---|---|
ref Required string, UUID | Your own reference for the transfer. Notabene never creates two transfers with the same ref, so a retry is safe. |
asset Required string | A symbol such as ETH, or a CAIP-19 identifier. Assets lists them. |
amount Required string, decimal | The amount in the asset’s whole units, for example "0.5" ETH, never its smallest unit. |
settlement_address Required string | The destination address, as a CAIP-10 identifier or a bare address on the asset’s chain. Notabene finds the counterparty institution from it. |
originator_id Required string | The id of your customer who sends the funds. |
beneficiary_id Required string | An id of your choosing for the beneficiary, for example a urn:uuid, or the id of a customer you registered. |
beneficiary.ivms object, IVMS-101 | The beneficiary’s name, when the Travel Rule asks for it. Data collection explains what to collect. |
beneficiary_entity_did Optional string, DID | The counterparty institution’s DID, when your customer picked it themselves. |
| Field | What to send |
|---|---|
ref (required), string, UUID | Your own reference for the transfer. Notabene never creates two transfers with the same ref, so a retry is safe. |
asset (required), string | A symbol such as ETH, or a CAIP-19 identifier. Assets lists them. |
amount (required), string, decimal | The amount in the asset’s whole units, for example "0.5" ETH, never its smallest unit. |
settlement_address (required), string | The destination address, as a CAIP-10 identifier or a bare address on the asset’s chain. Notabene finds the counterparty institution from it. |
originator_id (required), string | The id of your customer who sends the funds. |
beneficiary_id (required), string | An id of your choosing for the beneficiary, for example a urn:uuid, or the id of a customer you registered. |
beneficiary.ivms, object, IVMS-101 | The beneficiary’s name, when the Travel Rule asks for it. Data collection explains what to collect. |
beneficiary_entity_did (optional), string, DID | The counterparty institution’s DID, when your customer picked it themselves. |
The table lists the fields a withdrawal uses, and marks those a withdrawal requires. The API reference lists every field and marks only ref, asset and amount as always required, because deposits use a different set.
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.
2. Wait until it is ready to settle
Notabene now finds the counterparty institution, checks whether the Travel Rule applies, runs your policies within seconds and exchanges the Travel Rule data. Each time an institution authorizes the transfer, your webhook endpoint receives transfer.agent_status_changed.v3 with that institution in agentId. Once every institution is Authorized, the transfer is ready to settle. Events can repeat or arrive out of order, so handle them as described in Webhooks.
{
"eventType": "transfer.agent_status_changed.v3",
"payload": {
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"for": "did:web:originator.example:pt",
"agentId": "did:web:beneficiary.example:pt",
"fromStatus": "Pending",
"toStatus": "Authorized"
}
}
{
"eventType": "transfer.agent_status_changed.v3",
"payload": {
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"for": "did:web:originator.example:pt",
"agentId": "did:web:beneficiary.example:pt",
"fromStatus": "Pending",
"toStatus": "Authorized"
}
}If you would rather poll, read the transfer. The response holds the whole transfer, and the sample shows the fields a withdrawal reads, with asset given as its CAIP-19 identifier.
curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80 \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80', { 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/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, ) answer = None if response.status_code == 204 else response.json()
What to replace
8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80: The transferId from step 1
{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"status": "ReadyToSettle",
"direction": "Outgoing",
"asset": "eip155:1/slip44:60",
"amount": "0.5",
"counterparty": "did:web:beneficiary.example:pt",
"travel_rule_applies": true,
"requires_action": false
}curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80 \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80', {
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/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
)
answer = None if response.status_code == 204 else response.json(){
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"status": "ReadyToSettle",
"direction": "Outgoing",
"asset": "eip155:1/slip44:60",
"amount": "0.5",
"counterparty": "did:web:beneficiary.example:pt",
"travel_rule_applies": true,
"requires_action": false
}If the counterparty rejects the transfer, its status changes to Rejected and you do not send the funds.
If your policies flag the transfer
A flag holds the transfer for a person on your team, and nothing is sent to the counterparty until they decide. The status stays Active, and the transfer reads requires_action: true while it waits.
{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"status": "Active",
"direction": "Outgoing",
"requires_action": true
}
{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40",
"status": "Active",
"direction": "Outgoing",
"requires_action": true
}Your team finds the transfer in the dashboard under Inbox › Transactions and approves or rejects it there, or your backend does the same with the calls below. Approving uses force-authorize because it overrides your policies’ verdict, so every open requirement your institution set then reads Skipped. It needs a reason, which stays on the transfer’s timeline.
curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "reason": "Reviewed by compliance" }'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize', { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ "reason": "Reviewed by compliance" }), }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.post( 'https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, json={ "reason": "Reviewed by compliance" }, ) answer = None if response.status_code == 204 else response.json()
curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "reason": "Destination not allowed" }'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject', { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ "reason": "Destination not allowed" }), }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.post( 'https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, json={ "reason": "Destination not allowed" }, ) answer = None if response.status_code == 204 else response.json()
What to replace
8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80: The transferId from step 1Reviewed by compliance: Why your team approves it, kept on the transfer’s timelineDestination not allowed: Why your team rejects it, kept on the transfer’s timeline
{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Rejected",
"direction": "Outgoing",
"counterparty": "did:web:beneficiary.example:pt"
}curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"reason": "Reviewed by compliance"
}'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"reason": "Reviewed by compliance"
}),
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.post(
'https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/force-authorize',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
json={
"reason": "Reviewed by compliance"
},
)
answer = None if response.status_code == 204 else response.json()curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"reason": "Destination not allowed"
}'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"reason": "Destination not allowed"
}),
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.post(
'https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/reject',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
json={
"reason": "Destination not allowed"
},
)
answer = None if response.status_code == 204 else response.json(){
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Rejected",
"direction": "Outgoing",
"counterparty": "did:web:beneficiary.example:pt"
}Approving answers 204 with no body, and rejecting answers 200 with the transfer, now Rejected. A reason is optional when you reject.
If the counterparty does not answer
With counterparty coordination, your side waits up to 1 minute by default for the counterparty and then skips the requirements that wait on it, as described in Policies. The transfer itself stays Active until the counterparty authorizes it too, and its agents show which institution has not, as in Transfers.
Whether you send the funds at that point is your decision. If you do, record the hash as in step 3, and the transfer reads SettledIncompleteAuthorization until the counterparty authorizes it.
3. Send the funds and record the hash
Broadcast the transaction on chain, then record its hash on the transfer, which comes back Settled.
curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d" }'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle', { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ "settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d" }), }); const answer = response.status === 204 ? null : await response.json();import requests response = requests.post( 'https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle', headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}, json={ "settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d" }, ) answer = None if response.status_code == 204 else response.json()
What to replace
8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80: The transferId from step 1eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d: The transaction hash, as CAIP-220 or a bare hash on the asset’s chain
{
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Settled",
"settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"direction": "Outgoing"
}curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
}'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
}),
});
const answer = response.status === 204 ? null : await response.json();import requests
response = requests.post(
'https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/settle',
headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
json={
"settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
},
)
answer = None if response.status_code == 204 else response.json(){
"id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
"status": "Settled",
"settlement_id": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"direction": "Outgoing"
}Recording the same hash again answers 200, while a different hash answers 409. If you record the hash before every institution has authorized the transfer, it reads SettledIncompleteAuthorization until they all have.
When a call fails
Most errors carry a status, a short title and a detail that says what to correct. A request that fails validation lists each problem under errors instead.
{
"errors": [
{
"field": "ref",
"error": "INVALID_VALUE",
"message": "ref must be a UUID"
}
]
}
{
"title": "Conflict",
"status": 409,
"detail": "a transfer with this ref already exists with different content"
}
{
"errors": [
{
"field": "ref",
"error": "INVALID_VALUE",
"message": "ref must be a UUID"
}
]
}{
"title": "Conflict",
"status": 409,
"detail": "a transfer with this ref already exists with different content"
}| Answer | What it means |
|---|---|
400 with errors | A field is missing or invalid, for example originator or originator_id is required. field names it when it can. |
400 with unknown asset symbol | Notabene does not know the asset. Assets lists the ones it does. |
| 401 | The token is missing, malformed or expired. Authentication explains how to get a new one. |
409 with a transfer with this ref already exists with different content | A transfer with this ref exists. Sending a create again says what it means. |
409 with transfer already settled with a different settlement id | The transfer already has a different hash. |
| Answer | What it means |
|---|---|
400 with errors | A field is missing or invalid, for example originator or originator_id is required. field names it when it can. |
400 with unknown asset symbol | Notabene does not know the asset. Assets lists the ones it does. |
401 | The token is missing, malformed or expired. Authentication explains how to get a new one. |
409 with a transfer with this ref already exists with different content | A transfer with this ref exists. Sending a create again says what it means. |
409 with transfer already settled with a different settlement id | The transfer already has a different hash. |
Common mistakes
| Mistake | What happens | Instead |
|---|---|---|
| The amount in the asset’s smallest unit, such as wei | The amount is read in whole units, so 500000000000000000 means that many ETH. | Send "0.5" for 0.5 ETH, as Assets explains. |
An inline beneficiary without beneficiary_id | 400 with party profile provided without party ID. | Send beneficiary_id with every inline beneficiary. |
A new ref when you send a create again | A second transfer for the same funds. | Send the same request, with the same ref. |
| Approving a transfer straight after creating it | The approval answers 204, but your policies evaluate the transfer after it, and a flagged transfer is not sent. | Approve once your policies have flagged it, when the transfer reads requires_action: true. |
| Mistake | What happens | Instead |
|---|---|---|
| The amount in the asset’s smallest unit, such as wei | The amount is read in whole units, so 500000000000000000 means that many ETH. | Send "0.5" for 0.5 ETH, as Assets explains. |
An inline beneficiary without beneficiary_id | 400 with party profile provided without party ID. | Send beneficiary_id with every inline beneficiary. |
A new ref when you send a create again | A second transfer for the same funds. | Send the same request, with the same ref. |
| Approving a transfer straight after creating it | The approval answers 204, but your policies evaluate the transfer after it, and a flagged transfer is not sent. | Approve once your policies have flagged it, when the transfer reads requires_action: true. |
How it works
Done by Notabene
Your customer starts a withdrawal in your app by entering the destination address, the asset and the amount, and the beneficiary’s name when the Travel Rule asks for it.
- Data collection explains what to collect and how.
Your backend creates the transfer with a single call that names your customer, the destination and the beneficiary. The request, its answer and every field are in 1. Create the transfer.
Notabene finds the counterparty institution, checks whether the Travel Rule applies and runs your policies. Once your policies approve the transfer, Notabene sends the transfer details to the counterparty. It sends your customer’s personal data once the counterparty confirms that the destination address belongs to its customer.
- Your policies decide whether the transfer is approved, flagged for review or rejected.
- Authorization and sending are independent, so your side can be authorized before the personal data has been sent.
When the rules you configured approve the transfer and nothing else is open on your side, your side is authorized automatically, and there is no call to make.
- Your requirements preset is a separate setting. It decides whether your side also waits for the counterparty. With straight-through processing it does not. With counterparty coordination it waits for the counterparty’s address confirmation and authorization, for up to 1 minute by default.
When your policies flag the transfer, nothing is sent until a person on your team approves or rejects it, in the dashboard or through the API. The calls are in 2. Wait until it is ready to settle.
- Authorizing a transfer while a flag or a requirement is still open always counts as an override.
Each time an institution authorizes the transfer, your webhook endpoint receives transfer.agent_status_changed.v3. Once every institution is Authorized, it is ready to settle. The event and the polling call are in 2. Wait until it is ready to settle.
- When the destination is a self-hosted wallet, there is no counterparty to wait for, so the transfer is ready as soon as your side is authorized.
- You set up your webhook endpoint in the dashboard, under Settings › Webhooks.
Broadcast the transaction on chain, then record its hash on the transfer. The transfer is then Settled. The call is in 3. Send the funds and record the hash.
Optional calls
Most withdrawals never need the calls below, because creating the transfer already does this work. They help when your app needs an answer before the transfer exists, or when your customer cancels.
| Call | Use it when |
|---|---|
POST/ | Your app needs to show your customer what the Travel Rule requires before the transfer exists. See Data collection. |
POST/ | Your app needs to show which institution holds the destination address before the transfer exists. |
GET/ | Your customer has to pick the counterparty institution themselves. |
POST/ | Your customer cancels the withdrawal before it is settled. |
| Call | Use it when |
|---|---|
POST /api/v1/transfers/assess | Your app needs to show your customer what the Travel Rule requires before the transfer exists. See Data collection. |
POST /api/v1/wallet-ownerships/discover | Your app needs to show which institution holds the destination address before the transfer exists. |
GET /api/v1/network/entities | Your customer has to pick the counterparty institution themselves. |
POST /api/v1/transfers/{id}/cancel | Your customer cancels the withdrawal before it is settled. |
Related
Updated about 3 hours ago
