Docs
Webhooks & events
Every Noir on-chain event, signed webhook delivery, and how to verify the X-Noir-Signature header.
Event types
Every event also carries id (unique — dedupe on it), contractId, ledger, ledgerClosedAt and txHash.
| Type | Contract | Fields |
|---|---|---|
device.registered | device_registry | deviceHash, agent |
device.revoked | device_registry | deviceHash |
agent.registered | agent_registry | deviceHash, agent |
agent.revoked | agent_registry | deviceHash |
escrow.funded | payment_escrow | deviceHash, amount |
escrow.defunded | payment_escrow | deviceHash, amount |
escrow.swept | payment_escrow | deviceHash, amount |
payment.authorized | payment_escrow | deviceHash, merchant, amount |
payment.claimed | payment_escrow | merchant, amount |
Subscribe through the API
curl -X POST $NOIR_API/v1/webhooks -H "Authorization: Bearer $NOIR_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://shop.example/noir", "types": ["payment.authorized"], "merchant": "GMER…" }'
# → { "id": "wh_…", "secret": "whsec_…", … } store the secret: it is not shown againFilters are optional: types, deviceHash, merchant. One watcher on the gateway serves every subscription.
…or run the relay yourself
import { NoirAgentClient, startWebhookRelay } from '@noir-wallet/agent-sdk'
const stop = startWebhookRelay(new NoirAgentClient(), {
url: 'https://shop.example/noir', secret: process.env.NOIR_WEBHOOK_SECRET!,
types: ['payment.authorized'],
})Delivery
POST /noir HTTP/1.1
Content-Type: application/json
X-Noir-Signature: t=1760342400,v1=5f2b…c9
{"subscription":"wh_…","event":{"type":"payment.authorized","id":"0001…","deviceHash":"ab12…",
"merchant":"GMER…","amount":"10000000","ledger":512345,"txHash":"9f1c…", …}}- Amounts are decimal strings.
- At-least-once: retried with exponential backoff on network errors,
429and5xx. Any other4xxstops retries. - Respond
2xxquickly and do the work asynchronously.
Verify the signature
v1 is HMAC-SHA256 of `${t}.${rawBody}` with your secret, hex-encoded. Verify against the raw body and reject timestamps older than 5 minutes.
import { verifyWebhookSignature } from '@noir-wallet/agent-sdk'
app.post('/noir', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8')
if (!(await verifyWebhookSignature(SECRET, raw, req.header('x-noir-signature')))) return res.sendStatus(401)
const { event } = JSON.parse(raw)
if (await seen(event.id)) return res.sendStatus(200)
await markOrderPaid(event)
res.sendStatus(200)
})import hmac, hashlib, time
def verify(secret: str, raw: bytes, header: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts["v1"])Need history instead of push? Page through GET /v1/events or noir.getEvents; RPC keeps about seven days.