Webhooks & events
From settlement onward your platform is out of the transaction path. Webhooks are how you learn what happened without polling for it — settlements, dispute activity, verification decisions, and agent status changes, delivered to endpoints you control.
Webhooks tell you something changed. The reporting endpoints tell you the current state. Most platforms subscribe for responsiveness and reconcile against reporting on a schedule — a webhook you never received is invisible, but a record you can pull is not.
Endpoints
An endpoint is the unit of configuration: one URL, one environment, its own event selection, its own signing secret, its own delivery history. You can register several — routing settlements to one service and disputes to another is a normal setup, and it means a compromised secret exposes one destination rather than all of them.
| Field | Notes |
|---|---|
url | Must be HTTPS and publicly resolvable. Private, loopback, link-local and cloud metadata addresses are rejected. |
environment | sandbox or production. Immutable after creation — changing it means a new endpoint. |
events | The event types this endpoint receives. Editable at any time. |
secret | The signing secret. Readable and editable at any time; see below. |
active | Delivery can be paused without deleting the endpoint and losing its history. |
Environment is fixed at creation because a testnet settlement arriving at a production handler is the failure this separation exists to prevent. There is no patch that moves an endpoint between environments.
The signing secret
Your endpoint is a public URL accepting unauthenticated POSTs. TLS proves your
identity to Nustro; it proves nothing about Nustro’s identity to you. Anyone who
learns the URL could post a plausible dispute.resolved and watch your platform
act on it. The signature is what makes a payload trustworthy.
Nustro generates a secret when you create an endpoint. You may replace it with your own — useful when migrating from another provider or matching a value already in your secret manager — provided it is at least 32 characters.
Unlike your management key, the signing secret is not hashed — Nustro needs the plaintext to compute the HMAC on every delivery. It is therefore visible in the dashboard and on read, and there is no one-time reveal to miss. Editing the secret is how you rotate it; there is no separate rotation call. The change takes effect on the next delivery, so deploy the new value at the same time.
Changing an endpoint’s URL or its event selection leaves the secret untouched — your receiver keeps verifying without a redeploy.
Delivery format
Every delivery is a POST with a JSON body and three headers.
timestamp + "." + raw_body, keyed with the endpoint’s secret.{
"event_id": "evt_9c3f8a12d40b",
"type": "transaction.settled",
"created_at": "2026-05-14T09:12:44Z",
"environment": "sandbox",
"principal_id": "prn_7c4e21",
"agent_did": "did:aeap:d2146ca7-fbbd-4167-b725-b5ca2ebbb6da",
"data": {
"transaction_id": "txn_3a8e77",
"amount": "120.00",
"currency": "USDC",
"network": "base-sepolia"
}
}Every payload carries principal_id and agent_did, so your platform can route
the event to the right principal without a lookup.
Verifying a delivery
Three checks, in this order. Skipping any of them makes the signature decorative.
HMAC-SHA256(secret, timestamp + "." + raw_body), compared with a timing-safe equality function.import hmac, hashlib, time
def verify(raw_body, timestamp, signature, secret):
if abs(time.time() - int(timestamp)) > 300:
return False # stale — possible replay
signed = f"{timestamp}.{raw_body}".encode()
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)import crypto from 'node:crypto'
function verify(rawBody, timestamp, signature, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false // stale — possible replay
}
const signed = `${timestamp}.${rawBody}`
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}Retries and duplicates
| Behaviour | Detail |
|---|---|
| Success | Any 2xx. Respond fast — acknowledge first, process asynchronously. |
| Timeout | Around 10 seconds per attempt. |
| Retries | Exponential backoff for up to 24 hours, then the event is dropped. |
| Delivery guarantee | At least once. Duplicates are expected and normal. |
| Sustained failure | An endpoint failing continuously is disabled and surfaced in the dashboard rather than retried indefinitely. |
| Ordering | Not guaranteed. Use created_at if sequence matters to you. |
At-least-once delivery is a contract, not an edge case. Deduplicate on
Nustro-Event-Id and make your handler idempotent — a handler that credits a
principal twice on a redelivered transaction.settled is a bug in the
receiver, not in delivery.
Event types
| Event | Fires when | Why you want it |
|---|---|---|
platform.verification_decided | Your KYB review is accepted or rejected | The one event with no good polling alternative — it gates production access. |
transaction.settled | A transaction settles for one of your agents | Ledger updates, principal-facing activity, revenue reconciliation. |
dispute.filed | A dispute is opened against or by one of your agents | Notify the principal while the window is still open. |
dispute.resolved | A dispute reaches a decision | Escrow has moved; your records and the principal both need to know. |
agent.status_changed | An agent is activated, suspended, or revoked | A suspended agent cannot transact — stop routing to it. |
Once you switch on these strings, renaming one breaks every receiver. Treat additions as backwards compatible and removals as breaking, and ignore event types you do not recognise rather than failing on them — new types may be added.
Migrating from v0.2: receivers switching on customer.verification_decided
must update the string to platform.verification_decided — see the
changelog.
Operational notes
| Situation | What to do |
|---|---|
| Rotating a secret with no downtime | There is no dual-secret window. Either accept a brief verification gap, or add a second endpoint with the new secret, deploy, then delete the old one — both fire during the overlap, and your deduplication handles the rest. |
| Replacing an endpoint URL | Add the new endpoint first, confirm deliveries, then delete the old one. Deleting first loses every event fired in the gap; they are not queued. |
| New endpoints | Start from now. Past events are not replayed on creation. |
| Auditing changes | URL, secret, and event-selection changes are written to your activity log. A repointed endpoint is the one webhook change worth watching — signatures protect your receiver from forged senders, not your data from being sent somewhere new. |