Webhooks

Notabene sends webhooks to tell your backend when something happens on a transfer, so you do not have to poll for changes. You set up your endpoint once in the dashboard, and from then on your backend receives four events. Three keep your records in sync, and one tells you that Notabene is waiting for something from you.

Stay in sync
Act when asked
Steps
Stay in sync
Act when asked
← → the step before and after · Esc all steps
Step
Add your webhook endpoint
You, in the dashboard

In the dashboard, under Developers › Webhooks: Add your webhook endpoint.

← → the step before and after · Esc all steps
Step
Verify each delivery
Your backend

Check the Svix signature before anything else: Verify each delivery.

← → the step before and after · Esc all steps
Stay in sync
A transfer is created
Sent to both sides

A transfer exists on your node, yours or an incoming one: A transfer is created.

← → the step before and after · Esc all steps
Stay in sync
The status changes
Sent to both sides

The transaction status changed: The status changes.

← → the step before and after · Esc all steps
Stay in sync
An institution authorizes
Sent to both sides

An institution authorized the transfer, and once every one has, a withdrawal is ready to settle: An institution authorizes.

← → the step before and after · Esc all steps
Act when asked
Requirements are waiting
Sent to both sides

Requirements are open, and some may be yours: Requirements are waiting.

← → the step before and after · Esc all steps
Step
Read the transfer when you need the details
Your backend

Events carry no personal data, so read the transfer for the details: Read the transfer.

← → the step before and after · Esc all steps
  1. Add your webhook endpoint (You, in the dashboard). In the dashboard, under Developers › Webhooks: Add your webhook endpoint.
  2. Verify each delivery (Your backend). Check the Svix signature before anything else: Verify each delivery.
  3. Stay in sync: A transfer is created (transfer.created.v3, Sent to both sides). A transfer exists on your node, yours or an incoming one: A transfer is created.
  4. Stay in sync: The status changes (transfer.status_changed.v3, Sent to both sides). The transaction status changed: The status changes.
  5. Stay in sync: An institution authorizes (transfer.agent_status_changed.v3, Sent to both sides). An institution authorized the transfer, and once every one has, a withdrawal is ready to settle: An institution authorizes.
  6. Act when asked: Requirements are waiting (requirements.pending.v3, Sent to both sides). Requirements are open, and some may be yours: Requirements are waiting.
  7. Read the transfer when you need the details (GET /api/v1/transfers/{id}, Your backend). Events carry no personal data, so read the transfer for the details: Read the transfer.

Add your webhook endpoint

In the dashboard, go to Developers › Webhooks, add the HTTPS address of your endpoint and choose the events it should receive. Subscribe to transfer.created.v3, transfer.status_changed.v3, transfer.agent_status_changed.v3 and requirements.pending.v3.

Your institution has one endpoint, and it has its own signing secret, which starts with whsec_. You can reveal or rotate it on the endpoint’s page, and the previous secret keeps working for a while after a rotation.

Verify each delivery

Notabene delivers webhooks through Svix, and every delivery is signed. Check the signature with a Svix library before you use the content, then answer with a 2xx status.

const { Webhook } = await import("svix");

const wh = new Webhook(process.env.NOTABENE_WEBHOOK_SECRET);

// throws when the signature is not valid
wh.verify(rawBody, {
  "svix-id": headers["svix-id"],
  "svix-timestamp": headers["svix-timestamp"],
  "svix-signature": headers["svix-signature"],
});
const event = JSON.parse(rawBody);
import json
import os

from svix.webhooks import Webhook

wh = Webhook(os.environ["NOTABENE_WEBHOOK_SECRET"])
wh.verify(raw_body, {
    "svix-id": headers["svix-id"],
    "svix-timestamp": headers["svix-timestamp"],
    "svix-signature": headers["svix-signature"],
})  # raises WebhookVerificationError when the signature is not valid

event = json.loads(raw_body)
import svix "github.com/svix/svix-webhooks/go"

wh, err := svix.NewWebhook(os.Getenv("NOTABENE_WEBHOOK_SECRET"))
if err != nil {
	return err
}

// returns an error when the signature is not valid
if err := wh.Verify(rawBody, r.Header); err != nil {
	return err
}
var event map[string]any

err = json.Unmarshal(rawBody, &event)
const { Webhook } = await import("svix");

const wh = new Webhook(process.env.NOTABENE_WEBHOOK_SECRET);
// throws when the signature is not valid
wh.verify(rawBody, {
  "svix-id": headers["svix-id"],
  "svix-timestamp": headers["svix-timestamp"],
  "svix-signature": headers["svix-signature"],
});
const event = JSON.parse(rawBody);
import json
import os
from svix.webhooks import Webhook

wh = Webhook(os.environ["NOTABENE_WEBHOOK_SECRET"])
wh.verify(raw_body, {
    "svix-id": headers["svix-id"],
    "svix-timestamp": headers["svix-timestamp"],
    "svix-signature": headers["svix-signature"],
})  # raises WebhookVerificationError when the signature is not valid
event = json.loads(raw_body)
import svix "github.com/svix/svix-webhooks/go"

wh, err := svix.NewWebhook(os.Getenv("NOTABENE_WEBHOOK_SECRET"))
if err != nil {
	return err
}
// returns an error when the signature is not valid
if err := wh.Verify(rawBody, r.Header); err != nil {
	return err
}
var event map[string]any
err = json.Unmarshal(rawBody, &event)

Verify the raw request body, then parse it.

The four events

With these four events, your backend knows when a transfer arrives, when it needs to act and when a transfer is ready to settle, without polling.

EventSent toWhenWhat to do
transfer.created.v3Both sidesA transfer exists on your node, either one you created or a deposit that the originator has approved.Record it. Use direction to tell withdrawals from deposits.
transfer.status_changed.v3Both sidesThe transaction status changes.Keep your copy of the transfer in step, for example at Settled.
transfer.agent_status_changed.v3Both sidesAn institution on the transfer authorizes it, yours or the counterparty.Settle a withdrawal once every institution is Authorized.
requirements.pending.v3Both sidesRequirements are still open on the transfer.Resolve the ones that are yours, for example confirming a deposit address you did not register in advance.
EventSent toWhenWhat to do
transfer.created.v3Both sidesA transfer exists on your node, either one you created or a deposit that the originator has approved.Record it. Use direction to tell withdrawals from deposits.
transfer.status_changed.v3Both sidesThe transaction status changes.Keep your copy of the transfer in step, for example at Settled.
transfer.agent_status_changed.v3Both sidesAn institution on the transfer authorizes it, yours or the counterparty.Settle a withdrawal once every institution is Authorized.
requirements.pending.v3Both sidesRequirements are still open on the transfer.Resolve the ones that are yours, for example confirming a deposit address you did not register in advance.

The dashboard lists more event types than these four. Subscribe to the others when your integration needs them.

A transfer is created

Notabene sends transfer.created.v3 when a transfer exists on your node, either one you created or a deposit arriving from a counterparty institution. Use direction to tell them apart, and ref to match your own transfers with your records.

JSON
{
  "eventType": "transfer.created.v3",
  "payload": {
    "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
    "for": "did:web:originator.example:pt",
    "from": "did:web:originator.example:pt",
    "direction": "Outgoing",
    "counterparty": "did:web:beneficiary.example:pt",
    "asset": "eip155:1/slip44:60",
    "amount": "0.01",
    "ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40"
  }
}
{
  "eventType": "transfer.created.v3",
  "payload": {
    "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
    "for": "did:web:originator.example:pt",
    "from": "did:web:originator.example:pt",
    "direction": "Outgoing",
    "counterparty": "did:web:beneficiary.example:pt",
    "asset": "eip155:1/slip44:60",
    "amount": "0.01",
    "ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40"
  }
}

A deposit only arrives once the originator’s side has approved it.

The status changes

Notabene sends transfer.status_changed.v3 when the transaction status changes, with the previous status in fromStatus and the new one in toStatus. The transaction status combines the status of every institution on the transfer with the settlement.

  • If toStatus is Settled, the settlement was recorded, and the event includes settlementId.
  • If toStatus is Rejected or Canceled, the transfer stops there.
  • The event does not say whether your policies flagged the transfer. The transfer reads requires_action: true while it waits for your team.
JSON
{
  "eventType": "transfer.status_changed.v3",
  "payload": {
    "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
    "for": "did:web:originator.example:pt",
    "fromStatus": "ReadyToSettle",
    "toStatus": "Settled",
    "settlementId": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
  }
}
{
  "eventType": "transfer.status_changed.v3",
  "payload": {
    "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
    "for": "did:web:originator.example:pt",
    "fromStatus": "ReadyToSettle",
    "toStatus": "Settled",
    "settlementId": "eip155:1:tx/0x7d3c9e1f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
  }
}

Your own status, shown as My Status in the dashboard, is a separate value: read it as the status of your entry in the transfer’s agents. The event does not carry the reason for a rejection, which is in your audit trail. Statuses lists every status and what to do at each.

An institution authorizes

Notabene sends transfer.agent_status_changed.v3 when the status of an institution on the transfer changes, yours or the counterparty’s, with that institution in agentId. This is how you learn that a transfer is ready to settle: once every institution is Authorized, the originator’s institution sends the funds. The transaction status becomes ReadyToSettle at the same moment, so transfer.status_changed.v3 also arrives.

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"
  }
}

Here the beneficiary’s institution has authorized a withdrawal you sent, and yours already was, so you can send the funds.

Requirements are waiting

Notabene sends requirements.pending.v3 with the requirements that are still open on the transfer. Look at the ones whose agentId is your DID: agentId is the institution that must meet a requirement, and from the one that set it. Notabene handles some of them by itself, and the others need an answer from you.

  • If Notabene cannot match the deposit address to you, because you did not register it or confirm it before, WalletAddressConfirmation waits for your answer. Register the address with your customer, which confirms it, or reject it if it is not yours.
  • Once the address is confirmed, the Travel Rule data arrives and BeneficiaryNameMatch follows. Compare the name with your records.
  • If your setup includes an internal review, you also receive InternalComplianceCheck.
  • If the counterparty requires your authorization, Authorization lists your DID. You answer it by authorizing the transfer.

In this example you receive the transfer, and you answer the address confirmation by registering the address with your customer.

JSONJSON
API reference
{
  "eventType": "requirements.pending.v3",
  "payload": {
    "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
    "for": "did:web:beneficiary.example:pt",
    "requirements": [
      {
        "agentId": "did:web:beneficiary.example:pt",
        "from": "did:web:originator.example:pt",
        "id": "fea6ec34-b1b6-499e-8f5f-724a40ebce86",
        "status": "Pending",
        "type": "WalletAddressConfirmation"
      },
      {
        "agentId": "did:web:originator.example:pt",
        "from": "did:web:beneficiary.example:pt",
        "id": "d7697a6c-4fbd-4491-9580-8dd6a929d6e2",
        "status": "Pending",
        "type": "ReceiveTRInfo"
      }
    ]
  }
}
curl https://sandbox.notabene.id/api/v1/wallets \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
    "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
  }'const response = await fetch('https://sandbox.notabene.id/api/v1/wallets', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
    "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
  }),
});
const answer = response.status === 204 ? null : await response.json();import requests

response = requests.post(
    'https://sandbox.notabene.id/api/v1/wallets',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
      "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
    },
)
answer = None if response.status_code == 204 else response.json()
{
  "id": "did:pkh:eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
  "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
  "chain": "eip155:1",
  "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27",
  "created_at": "2026-10-20T09:14:03Z"
}
What to replace
  • eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed: The deposit address, as CAIP-10
  • urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27: The id of your customer who owns the address
{
  "eventType": "requirements.pending.v3",
  "payload": {
    "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
    "for": "did:web:beneficiary.example:pt",
    "requirements": [
      {
        "agentId": "did:web:beneficiary.example:pt",
        "from": "did:web:originator.example:pt",
        "id": "fea6ec34-b1b6-499e-8f5f-724a40ebce86",
        "status": "Pending",
        "type": "WalletAddressConfirmation"
      },
      {
        "agentId": "did:web:originator.example:pt",
        "from": "did:web:beneficiary.example:pt",
        "id": "d7697a6c-4fbd-4491-9580-8dd6a929d6e2",
        "status": "Pending",
        "type": "ReceiveTRInfo"
      }
    ]
  }
}
curl https://sandbox.notabene.id/api/v1/wallets \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
    "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
  }'
const response = await fetch('https://sandbox.notabene.id/api/v1/wallets', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
    "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
  }),
});
const answer = response.status === 204 ? null : await response.json();
import requests

response = requests.post(
    'https://sandbox.notabene.id/api/v1/wallets',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
      "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27"
    },
)
answer = None if response.status_code == 204 else response.json()
{
  "id": "did:pkh:eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
  "address": "eip155:1:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
  "chain": "eip155:1",
  "customer_id": "urn:uuid:3c9a7e52-41f6-4b8d-a0e2-6d1f9b4c8a27",
  "created_at": "2026-10-20T09:14:03Z"
}

Only the first requirement is yours, while the second asks the counterparty to send its Travel Rule data. Requirements you resolve lists every requirement you resolve and its answers.

Requirements you resolve

A requirements.pending.v3 event lists every open requirement, but only those whose agentId is your DID concern you, and only some of those need a call. You resolve each of them with the same call, POST /api/v1/transfers/{id}/requirements/{requirement}/resolve, where {requirement} is the name in the second column and the body holds your outcome.

API reference
curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/beneficiary-name-match/resolve \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "outcome": "matched"
  }'const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/beneficiary-name-match/resolve', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "outcome": "matched"
  }),
});
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/requirements/beneficiary-name-match/resolve',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "outcome": "matched"
    },
)
answer = None if response.status_code == 204 else response.json()
What to replace
  • 8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80: The transfer’s id, from the event
  • beneficiary-name-match: The name in the second column of the table below
  • matched: One of the outcomes in the table below
Response204
HTTP/1.1 204 No Content
curl https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/beneficiary-name-match/resolve \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "outcome": "matched"
  }'
const response = await fetch('https://sandbox.notabene.id/api/v1/transfers/8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80/requirements/beneficiary-name-match/resolve', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "outcome": "matched"
  }),
});
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/requirements/beneficiary-name-match/resolve',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "outcome": "matched"
    },
)
answer = None if response.status_code == 204 else response.json()
HTTP/1.1 204 No Content
TypeIn the pathWhat it asksOutcome
WalletAddressConfirmationwallet-address-confirmationConfirm that the deposit address belongs to your customer. You are not asked again for an address you confirmed before.confirmed or rejected, as below.
BeneficiaryNameMatchbeneficiary-name-matchCompare the beneficiary’s name with your records. It follows the address confirmation, once the Travel Rule data has arrived.matched, partial_match (holds the transfer for review) or mismatch.
InternalComplianceCheckinternal-compliance-checkComplete your own compliance review.completed or rejected.
SubmitTRInfoAdd the sender’s data yourself, because the institution you asked did not answer. It appears only when you turned this on in the dashboard.Nothing to send here. Deposits, funds first has the calls.
TransmitTRData, ReceiveTRInfo, WalletConfirmationForDisclosureSend or receive the Travel Rule data, or wait for the counterparty’s confirmation.Nothing to send. Notabene and the counterparty handle these.
TypeIn the pathWhat it asksOutcome
WalletAddressConfirmationwallet-address-confirmationConfirm that the deposit address belongs to your customer. You are not asked again for an address you confirmed before.confirmed or rejected, as below.
BeneficiaryNameMatchbeneficiary-name-matchCompare the beneficiary’s name with your records. It follows the address confirmation, once the Travel Rule data has arrived.matched, partial_match (holds the transfer for review) or mismatch.
InternalComplianceCheckinternal-compliance-checkComplete your own compliance review.completed or rejected.
SubmitTRInfoAdd the sender’s data yourself, because the institution you asked did not answer. It appears only when you turned this on in the dashboard.Nothing to send here. Deposits, funds first has the calls.
TransmitTRData, ReceiveTRInfo, WalletConfirmationForDisclosureSend or receive the Travel Rule data, or wait for the counterparty’s confirmation.Nothing to send. Notabene and the counterparty handle these.

You rarely answer the address confirmation yourself. Notabene satisfies it by itself when you registered the address or confirmed it before, and registering the address now with POST /api/v1/wallets satisfies it too.

To confirm the address for this transfer only, without registering it, answer confirmed. When the address is not yours, answer rejected, which rejects the transfer with the reason code AC03, and the counterparty then stops treating the address as yours.

Read the transfer

Webhooks carry identifiers and statuses but never personal data. When you need the full picture, for example the originator and the beneficiary, or the requirements that are still open, read the transfer. Its next_action says what your backend does next, as Statuses and decisions in the API explains.

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 transfer’s id, from the event
Response200
{
  "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
  "type": "Transfer",
  "status": "Active",
  "direction": "Incoming",
  "counterparty": "did:web:originator.example:pt",
  "asset": "eip155:1/slip44:60",
  "amount": "0.5",
  "requires_action": true,
  "next_action": {
    "type": "Review"
  },
  "agents": [
    {
      "agent_did": "did:web:originator.example:pt",
      "role": "VASP",
      "status": "Authorized",
      "policies": [
        {
          "type": "WalletAddressConfirmation",
          "status": "Satisfied",
          "from_did": "did:web:beneficiary.example:pt"
        }
      ]
    },
    {
      "agent_did": "did:web:beneficiary.example:pt",
      "role": "VASP",
      "status": "Pending",
      "policies": [
        {
          "type": "ReceiveTRInfo",
          "status": "Pending",
          "from_did": "did:web:originator.example:pt"
        },
        {
          "type": "Authorization",
          "status": "Satisfied",
          "from_did": "did:web:originator.example:pt"
        },
        {
          "type": "BeneficiaryNameMatch",
          "status": "Pending",
          "from_did": "did:web:beneficiary.example:pt"
        }
      ]
    }
  ]
}
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",
  "type": "Transfer",
  "status": "Active",
  "direction": "Incoming",
  "counterparty": "did:web:originator.example:pt",
  "asset": "eip155:1/slip44:60",
  "amount": "0.5",
  "requires_action": true,
  "next_action": {
    "type": "Review"
  },
  "agents": [
    {
      "agent_did": "did:web:originator.example:pt",
      "role": "VASP",
      "status": "Authorized",
      "policies": [
        {
          "type": "WalletAddressConfirmation",
          "status": "Satisfied",
          "from_did": "did:web:beneficiary.example:pt"
        }
      ]
    },
    {
      "agent_did": "did:web:beneficiary.example:pt",
      "role": "VASP",
      "status": "Pending",
      "policies": [
        {
          "type": "ReceiveTRInfo",
          "status": "Pending",
          "from_did": "did:web:originator.example:pt"
        },
        {
          "type": "Authorization",
          "status": "Satisfied",
          "from_did": "did:web:originator.example:pt"
        },
        {
          "type": "BeneficiaryNameMatch",
          "status": "Pending",
          "from_did": "did:web:beneficiary.example:pt"
        }
      ]
    }
  ]
}

Add include_ivms=true to include the originator’s and the beneficiary’s personal data.

Poll for events

When your systems cannot accept incoming calls, a Svix polling endpoint gives you the same events for your backend to fetch. You subscribe it to the same four events, and each fetch returns the events your consumer has not committed yet. Create it under Developers › Webhooks, where you would add a webhook endpoint, by generating a polling endpoint instead of entering a URL.

HTTPJSONHTTP
GET https://api.eu.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/backend-1
Authorization: Bearer sk_poll_...
{
  "data": [
    {
      "id": "msg_2K2N9Qk7xYb3LmQe8Rt5Vw1Za",
      "eventType": "transfer.created.v3",
      "payload": {
        "eventType": "transfer.created.v3",
        "payload": {
          "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
          "for": "did:web:beneficiary.example:pt",
          "from": "did:web:originator.example:pt",
          "direction": "Incoming",
          "counterparty": "did:web:originator.example:pt",
          "asset": "eip155:1/slip44:60",
          "amount": "0.01",
          "ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40"
        }
      },
      "timestamp": "2026-11-06T10:15:02.000Z",
      "offset": 41
    }
  ],
  "done": true
}
POST https://api.eu.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/backend-1/commit
Authorization: Bearer sk_poll_...

{
  "offset": 41
}
GET https://api.eu.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/backend-1
Authorization: Bearer sk_poll_...
{
  "data": [
    {
      "id": "msg_2K2N9Qk7xYb3LmQe8Rt5Vw1Za",
      "eventType": "transfer.created.v3",
      "payload": {
        "eventType": "transfer.created.v3",
        "payload": {
          "id": "8f3c2a1e-5b7d-4c90-a1f2-3e4d5c6b7a80",
          "for": "did:web:beneficiary.example:pt",
          "from": "did:web:originator.example:pt",
          "direction": "Incoming",
          "counterparty": "did:web:originator.example:pt",
          "asset": "eip155:1/slip44:60",
          "amount": "0.01",
          "ref": "0b6a4f8e-2d1c-4e7a-9b3f-5c8d2e1a7f40"
        }
      },
      "timestamp": "2026-11-06T10:15:02.000Z",
      "offset": 41
    }
  ],
  "done": true
}
POST https://api.eu.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/backend-1/commit
Authorization: Bearer sk_poll_...

{
  "offset": 41
}

Events come back in the order they were sent, each with an offset. Once you have handled a batch, commit the last offset, so the next fetch starts after it, because events you have not committed come back again. Keep the same consumer id for a worker, since each consumer id keeps its own place. Svix’s guide to polling endpoints has the details.

The envelope and delivery

Every delivery has the same envelope as the events above: the event type and a payload. The payload always includes the transfer id, and for, the DID of your institution that the delivery is for. Most of its fields are written in camelCase, such as toStatus. The settlement hash is settlement_id in transfer.created.v3 and settlementId in transfer.status_changed.v3.

Deliveries are signed by Svix and retried until your endpoint answers with a 2xx status. The same delivery can arrive more than once with the same svix-id header, so ignore repeats. Deliveries are not guaranteed to arrive in order, so rely on the status in the event, or read the transfer, rather than on the order in which events arrive.


Did this page help you?