Developers

Two webhooks.
Signed, and yours.

The entire integration, no signup wall. Every event is an HMAC-SHA256 signature over the raw body, keyed with your secret. Recompute it, compare, done. If your server doesn't 200, we retry with backoff until it does.

payment_detected · 0-confpayment_confirmed · 10-conf
01

Share your view key

Onboard with your wallet's private view key, never the spend key. We watch payments arrive; we mathematically cannot move them.

02

Create an invoice

Price it in XMR, or in USD/EUR with the rate locked at creation. Each invoice mints its own subaddress and a hosted checkout page with a QR and monero: URI.

03

Receive signed webhooks

payment_detected at 0-conf, payment_confirmed at 10. Every payload HMAC-SHA256 signed, retried with backoff until your server 200s.

The payload you'll receive.

A single POST to the URL you register, on every state change. Amounts are exact XMR strings; rate and price_* echo the fiat lock from invoice creation so your books reconcile without a second lookup.

POST /your/webhook
X-Webhook-Event: payment_confirmed
X-Webhook-Signature: hmac-sha256=f889d2ae…ec23a
{
  "event": "payment_confirmed",
  "invoice_id": "b1a44b4c-…-b1d5b4248ccd",
  "amount_due_xmr": "0.000917431193",
  "amount_received_xmr": "0.000917431193",
  "price_amount": "0.30", "price_currency": "USD",
  "rate": "327",
  "txids": ["5faf1d15…99717"],
  "confirmations": 10,
  "timestamp": "2026-07-05T09:25:11.433Z"
}

Verify before you trust.

Compute HMAC-SHA256 over the raw request body with your webhook secret and compare in constant time. Never parse JSON before verifying. Rotate the secret any time from your dashboard, old and new overlap during a grace window.

verify.ts
import crypto from "crypto";

// req.body must be the RAW bytes, not parsed JSON
const expected = "hmac-sha256=" +
  crypto.createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody).digest("hex");

const got = req.headers["x-webhook-signature"];
const ok = got.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));

if (!ok) return res.status(401).end();

Events & invoice lifecycle

payment_detected0-conf

The invoice's full amount was seen in the mempool or a fresh block. Fast, optimistic, treat as “incoming,” not final.

payment_confirmed10-conf

The payment reached the confirmation threshold (default 10). Safe to fulfill. This is the settlement signal.

pending

Invoice created, awaiting payment. Expires after its window (5–1440 min, default 60).

detected

Amount seen at 0-conf. payment_detected fired.

confirmed

≥ threshold confirmations. payment_confirmed fired. Terminal.

expired

Window elapsed with no sufficient payment. Terminal.

reverted

A detected 0-conf payment dropped from the chain (reorg / replaced). The invoice falls back to pending and its payment_detected is withdrawn.

Scope: Umbra settles Monero in, non-custodially, you keep the spend key, funds land in your wallet. It is not a card or fiat processor. Retries use exponential backoff (up to 8 attempts); every delivery is logged and can be redelivered from the dashboard.

Ship it.

Create an account, drop in your view key, point a webhook. You'll be settling real Monero in minutes.