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.
In the dashboard, under Developers › Webhooks: Add your webhook endpoint.
Check the Svix signature before anything else: Verify each delivery.
A transfer exists on your node, yours or an incoming one: A transfer is created.
The transaction status changed: The status changes.
An institution authorized the transfer, and once every one has, a withdrawal is ready to settle: An institution authorizes.
Requirements are open, and some may be yours: Requirements are waiting.
Events carry no personal data, so read the transfer for the details: Read the transfer.
- Add your webhook endpoint (You, in the dashboard). In the dashboard, under Developers › Webhooks: Add your webhook endpoint.
- Verify each delivery (Your backend). Check the Svix signature before anything else: Verify each delivery.
- 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. - Stay in sync: The status changes (
transfer.status_changed.v3, Sent to both sides). The transaction status changed: The status changes. - 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. - Act when asked: Requirements are waiting (
requirements.pending.v3, Sent to both sides). Requirements are open, and some may be yours: Requirements are waiting. - 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.
| Event | Sent to | When | What to do |
|---|---|---|---|
transfer.created.v3 | Both sides | A 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.v3 | Both sides | The transaction status changes. | Keep your copy of the transfer in step, for example at Settled. |
transfer.agent_status_changed.v3 | Both sides | An institution on the transfer authorizes it, yours or the counterparty. | Settle a withdrawal once every institution is Authorized. |
requirements.pending.v3 | Both sides | Requirements are still open on the transfer. | Resolve the ones that are yours, for example confirming a deposit address you did not register in advance. |
| Event | Sent to | When | What to do |
|---|---|---|---|
transfer.created.v3 | Both sides | A 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.v3 | Both sides | The transaction status changes. | Keep your copy of the transfer in step, for example at Settled. |
transfer.agent_status_changed.v3 | Both sides | An institution on the transfer authorizes it, yours or the counterparty. | Settle a withdrawal once every institution is Authorized. |
requirements.pending.v3 | Both sides | Requirements 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.
{
"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
toStatusisSettled, the settlement was recorded, and the event includessettlementId. - If
toStatusisRejectedorCanceled, the transfer stops there. - The event does not say whether your policies flagged the transfer. The transfer reads
requires_action: truewhile it waits for your team.
{
"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.
{
"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,
WalletAddressConfirmationwaits 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
BeneficiaryNameMatchfollows. Compare the name with your records. - If your setup includes an internal review, you also receive
InternalComplianceCheck. - If the counterparty requires your authorization,
Authorizationlists 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.
{
"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-10urn: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.
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 eventbeneficiary-name-match: The name in the second column of the table belowmatched: One of the outcomes in the table below
HTTP/1.1 204 No Contentcurl 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| Type | In the path | What it asks | Outcome |
|---|---|---|---|
WalletAddressConfirmation | wallet-address-confirmation | Confirm that the deposit address belongs to your customer. You are not asked again for an address you confirmed before. | confirmed or rejected, as below. |
BeneficiaryNameMatch | beneficiary-name-match | Compare 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. |
InternalComplianceCheck | internal-compliance-check | Complete your own compliance review. | completed or rejected. |
SubmitTRInfo | Add 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, WalletConfirmationForDisclosure | Send or receive the Travel Rule data, or wait for the counterparty’s confirmation. | Nothing to send. Notabene and the counterparty handle these. |
| Type | In the path | What it asks | Outcome |
|---|---|---|---|
WalletAddressConfirmation | wallet-address-confirmation | Confirm that the deposit address belongs to your customer. You are not asked again for an address you confirmed before. | confirmed or rejected, as below. |
BeneficiaryNameMatch | beneficiary-name-match | Compare 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. |
InternalComplianceCheck | internal-compliance-check | Complete your own compliance review. | completed or rejected. |
SubmitTRInfo | Add 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, WalletConfirmationForDisclosure | Send 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.
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
{
"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.
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.
Updated 3 days ago
