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.

TypeContractFields
device.registereddevice_registrydeviceHash, agent
device.revokeddevice_registrydeviceHash
agent.registeredagent_registrydeviceHash, agent
agent.revokedagent_registrydeviceHash
escrow.fundedpayment_escrowdeviceHash, amount
escrow.defundedpayment_escrowdeviceHash, amount
escrow.sweptpayment_escrowdeviceHash, amount
payment.authorizedpayment_escrowdeviceHash, merchant, amount
payment.claimedpayment_escrowmerchant, amount

Subscribe through the API

bash
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 again

Filters are optional: types, deviceHash, merchant. One watcher on the gateway serves every subscription.

…or run the relay yourself

ts
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

Request your endpoint receives
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, 429 and 5xx. Any other 4xx stops retries.
  • Respond 2xx quickly 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.

Node / TypeScript
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)
})
Python
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.