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

API reference
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 transfer
  • eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed: The destination address your customer entered
  • urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27: The id of your customer who sends the funds
  • urn:uuid:e7d14b39-8a2c-4f05-b6e1-2c9f7a3d5b18: An id of your choosing for the beneficiary
  • Smith: The beneficiary’s last name
  • Alice: The beneficiary’s first names
Response201
{
  "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.

FieldWhat to send
ref Required string, UUIDYour own reference for the transfer. Notabene never creates two transfers with the same ref, so a retry is safe.
asset Required stringA symbol such as ETH, or a CAIP-19 identifier. Assets lists them.
amount Required string, decimalThe amount in the asset’s whole units, for example "0.5" ETH, never its smallest unit.
settlement_address Required stringThe 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 stringThe id of your customer who sends the funds.
beneficiary_id Required stringAn id of your choosing for the beneficiary, for example a urn:uuid, or the id of a customer you registered.
beneficiary.ivms object, IVMS-101The beneficiary’s name, when the Travel Rule asks for it. Data collection explains what to collect.
beneficiary_entity_did Optional string, DIDThe counterparty institution’s DID, when your customer picked it themselves.
FieldWhat to send
ref (required), string, UUIDYour own reference for the transfer. Notabene never creates two transfers with the same ref, so a retry is safe.
asset (required), stringA symbol such as ETH, or a CAIP-19 identifier. Assets lists them.
amount (required), string, decimalThe amount in the asset’s whole units, for example "0.5" ETH, never its smallest unit.
settlement_address (required), stringThe 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), stringThe id of your customer who sends the funds.
beneficiary_id (required), stringAn id of your choosing for the beneficiary, for example a urn:uuid, or the id of a customer you registered.
beneficiary.ivms, object, IVMS-101The beneficiary’s name, when the Travel Rule asks for it. Data collection explains what to collect.
beneficiary_entity_did (optional), string, DIDThe 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.

JSON
{
  "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.

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

JSON
{
  "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 1
  • Reviewed by compliance: Why your team approves it, kept on the transfer’s timeline
  • Destination not allowed: Why your team rejects it, kept on the transfer’s timeline
Reject response200
{
  "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.

API reference
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 1
  • eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d: The transaction hash, as CAIP-220 or a bare hash on the asset’s chain
Response200
{
  "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.

JSONJSON
{
  "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"
}
AnswerWhat it means
400 with errorsA field is missing or invalid, for example originator or originator_id is required. field names it when it can.
400 with unknown asset symbolNotabene does not know the asset. Assets lists the ones it does.
401The 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 contentA transfer with this ref exists. Sending a create again says what it means.
409 with transfer already settled with a different settlement idThe transfer already has a different hash.
AnswerWhat it means
400 with errorsA field is missing or invalid, for example originator or originator_id is required. field names it when it can.
400 with unknown asset symbolNotabene does not know the asset. Assets lists the ones it does.
401The 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 contentA transfer with this ref exists. Sending a create again says what it means.
409 with transfer already settled with a different settlement idThe transfer already has a different hash.

Common mistakes

MistakeWhat happensInstead
The amount in the asset’s smallest unit, such as weiThe 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_id400 with party profile provided without party ID.Send beneficiary_id with every inline beneficiary.
A new ref when you send a create againA second transfer for the same funds.Send the same request, with the same ref.
Approving a transfer straight after creating itThe 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.
MistakeWhat happensInstead
The amount in the asset’s smallest unit, such as weiThe 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_id400 with party profile provided without party ID.Send beneficiary_id with every inline beneficiary.
A new ref when you send a create againA second transfer for the same funds.Send the same request, with the same ref.
Approving a transfer straight after creating itThe 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

Automatic
Manual review
Steps
Automatic
Manual review

Done by Notabene

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

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.

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

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.

← → the step before and after · Esc all steps
Step
Notabene handles the Travel Rule
Notabene, as soon as the transfer exists

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.

Good to know
  • 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.
← → the step before and after · Esc all steps
Automatic
Your policies approve it automatically
Your policy engine, with the rules you configured

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.

Good to know
  • 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.
← → the step before and after · Esc all steps
Manual review
Your team reviews a flagged transfer
Your compliance team

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.

Good to know
  • Authorizing a transfer while a flag or a requirement is still open always counts as an override.
← → the step before and after · Esc all steps
Step 2
2Notabene tells you it is ready to settle
Notabene, to your webhook endpoint

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.

Good to know
  • 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.
← → the step before and after · Esc all steps
Step 3
3Send the funds and record the hash
Your wallet, then your backend

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.

← → the step before and after · Esc all steps

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.

CallUse it when
POST/api/v1/transfers/assessYour app needs to show your customer what the Travel Rule requires before the transfer exists. See Data collection.
POST/api/v1/wallet-ownerships/discoverYour app needs to show which institution holds the destination address before the transfer exists.
GET/api/v1/network/entitiesYour customer has to pick the counterparty institution themselves.
POST/api/v1/transfers/{id}/cancelYour customer cancels the withdrawal before it is settled.
CallUse it when
POST /api/v1/transfers/assessYour app needs to show your customer what the Travel Rule requires before the transfer exists. See Data collection.
POST /api/v1/wallet-ownerships/discoverYour app needs to show which institution holds the destination address before the transfer exists.
GET /api/v1/network/entitiesYour customer has to pick the counterparty institution themselves.
POST /api/v1/transfers/{id}/cancelYour customer cancels the withdrawal before it is settled.

Related

  • Transfers: what each institution sees on a transfer, and why statuses differ.
  • Statuses: every status and what your backend does at each.
  • Webhooks: every event and every requirement you may have to answer.
  • Errors: every error message and what to do.

Did this page help you?