Updated October 2026: all code is now inline, uses .NET 8+ with System.Text.Json and IHttpClientFactory, and the IPN signature check has been rewritten to sort nested JSON correctly and compare in constant time.
This tutorial is for educational purposes. Make sure accepting Monero (XMR) is legal where you operate; you are responsible for your use of it.
Quick Answer
- Does NOWPayments support Monero? Yes. Set
pay_currencytoxmrwhen you create an invoice or payment, and the funds are sent on to your own Monero wallet. - Create an invoice:
POST https://api.nowpayments.io/v1/invoicewith your key in thex-api-keyheader. The response includes aninvoice_url; redirect the buyer there. - Verify IPN callbacks: NOWPayments sends an
x-nowpayments-sigheader. It is the lowercase hex HMAC-SHA512 of the request body, re-serialized with its keys sorted alphabetically, keyed with your IPN secret. - When is it paid? Treat
finishedas paid.waiting,confirming,confirmedandsendingare in-progress states.
What Is Monero (XMR)?
Monero is a privacy-focused cryptocurrency. Its blockchain hides the sender, the receiver and the amount of every transaction, so a merchant can't see a buyer's balance or history, and outside observers can't profile either party. Payments can't be blocked by a card network, and there are no chargebacks.
What Is NOWPayments?
NOWPayments is a non-custodial crypto payment gateway. It gives you hosted invoice pages, exchange-rate quotes from fiat prices, and status callbacks, then forwards the coins to a wallet you control (minus a small fee). That makes it a low-effort way to accept Monero without running your own node and wallet. (If you'd rather self-host, see the monero-wallet-rpc guide.)
How the Payment Flow Works
- Your app creates an invoice for a fiat price (for example, $20 USD) with your own
order_id. - NOWPayments returns an
invoice_url. You redirect the buyer there; the page shows the Monero address, a QR code and the XMR amount. - As the payment moves along, NOWPayments POSTs IPN (Instant Payment Notification) messages to your
ipn_callback_url. - You verify each message's signature, look up the order by
order_id, and fulfill it once the status isfinished.
Step 1: Set Up Your Wallet and NOWPayments Account
- Monero wallet: if you don't have one, use the official Monero GUI wallet or another reputable wallet. Write the seed phrase down offline; anyone with it controls the funds. Copy your primary address (it starts with a 4).
- NOWPayments account: sign up, add your Monero address as the payout wallet, and choose XMR as an accepted coin.
- API key: create one in the dashboard's payment settings. It goes in the
x-api-keyheader of every API request. - IPN secret: generate the Instant Payment Notifications secret in the same area. You use it only to verify incoming callbacks; it is never sent in a request.
Keep both values out of source control: use user secrets in development and environment variables or a protected appsettings.Production.json in production.
{
"NowPayments": {
"ApiKey": "set-in-user-secrets-or-environment",
"IpnSecret": "set-in-user-secrets-or-environment",
"IpnCallbackUrl": "https://example.com/payments/nowpayments/ipn",
"SuccessUrl": "https://example.com/checkout/success",
"CancelUrl": "https://example.com/checkout/cancel"
}
}
Step 2: Models and Options
No third-party NuGet packages are needed; System.Text.Json and HttpClient are built in. First, an options class bound from configuration:
public sealed class NowPaymentsOptions
{
public string ApiKey { get; set; } = ""; // Dashboard > Settings > Payments > API keys
public string IpnSecret { get; set; } = ""; // Dashboard > Settings > Payments > Instant payment notifications
public string IpnCallbackUrl { get; set; } = ""; // e.g. https://example.com/payments/nowpayments/ipn
public string SuccessUrl { get; set; } = "";
public string CancelUrl { get; set; } = "";
}
Then the request and response models. The JSON names match the NOWPayments API exactly:
using System.Text.Json.Serialization;
public sealed class CreateInvoiceRequest
{
[JsonPropertyName("price_amount")] public decimal PriceAmount { get; set; }
[JsonPropertyName("price_currency")] public string PriceCurrency { get; set; } = "usd";
[JsonPropertyName("pay_currency")] public string? PayCurrency { get; set; } = "xmr";
[JsonPropertyName("order_id")] public string? OrderId { get; set; }
[JsonPropertyName("order_description")] public string? OrderDescription { get; set; }
[JsonPropertyName("ipn_callback_url")] public string? IpnCallbackUrl { get; set; }
[JsonPropertyName("success_url")] public string? SuccessUrl { get; set; }
[JsonPropertyName("cancel_url")] public string? CancelUrl { get; set; }
[JsonPropertyName("is_fee_paid_by_user")] public bool? IsFeePaidByUser { get; set; }
}
public sealed class InvoiceResponse
{
[JsonPropertyName("id")] public string? Id { get; set; }
[JsonPropertyName("order_id")] public string? OrderId { get; set; }
[JsonPropertyName("invoice_url")] public string? InvoiceUrl { get; set; }
[JsonPropertyName("price_amount")] public string? PriceAmount { get; set; }
[JsonPropertyName("price_currency")] public string? PriceCurrency { get; set; }
[JsonPropertyName("pay_currency")] public string? PayCurrency { get; set; }
[JsonPropertyName("created_at")] public string? CreatedAt { get; set; }
}
public sealed class IpnPaymentMessage
{
[JsonPropertyName("payment_id")] public long PaymentId { get; set; }
[JsonPropertyName("invoice_id")] public long? InvoiceId { get; set; }
[JsonPropertyName("payment_status")] public string PaymentStatus { get; set; } = "";
[JsonPropertyName("pay_address")] public string? PayAddress { get; set; }
[JsonPropertyName("price_amount")] public decimal PriceAmount { get; set; }
[JsonPropertyName("price_currency")] public string? PriceCurrency { get; set; }
[JsonPropertyName("pay_amount")] public decimal PayAmount { get; set; }
[JsonPropertyName("actually_paid")] public decimal ActuallyPaid { get; set; }
[JsonPropertyName("pay_currency")] public string? PayCurrency { get; set; }
[JsonPropertyName("order_id")] public string? OrderId { get; set; }
[JsonPropertyName("outcome_amount")] public decimal? OutcomeAmount { get; set; }
[JsonPropertyName("outcome_currency")] public string? OutcomeCurrency { get; set; }
}
Step 3: Create a Monero Invoice
A typed HttpClient keeps the base address and API key in one place. The amount is a fiat price (price_amount + price_currency); NOWPayments converts it to XMR at payment time. You cannot request a fixed amount of XMR through the invoice endpoint.
using System.Net.Http.Json;
using Microsoft.Extensions.Options;
public sealed class NowPaymentsClient(HttpClient http, IOptions<NowPaymentsOptions> options)
{
private readonly NowPaymentsOptions _options = options.Value;
public async Task<InvoiceResponse> CreateInvoiceAsync(
decimal priceUsd, string orderId, string description, CancellationToken ct = default)
{
var request = new CreateInvoiceRequest
{
PriceAmount = priceUsd,
PriceCurrency = "usd",
PayCurrency = "xmr", // ask for Monero specifically
OrderId = orderId, // YOUR id; comes back in every IPN
OrderDescription = description,
IpnCallbackUrl = _options.IpnCallbackUrl,
SuccessUrl = _options.SuccessUrl,
CancelUrl = _options.CancelUrl
};
using HttpResponseMessage response = await http.PostAsJsonAsync("v1/invoice", request, ct);
string body = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
// NOWPayments returns a JSON error body (e.g. amount too small, currency unavailable).
throw new HttpRequestException(
$"NOWPayments invoice failed ({(int)response.StatusCode}): {body}");
}
return System.Text.Json.JsonSerializer.Deserialize<InvoiceResponse>(body)
?? throw new InvalidOperationException("Empty invoice response.");
}
}
If the call fails, log the response body. Common errors are a missing or wrong x-api-key, a currency that isn't enabled in your account, and an amount below the coin's minimum. You can query the minimum with GET /v1/min-amount?currency_from=xmr¤cy_to=xmr.
Step 4: Verify the x-nowpayments-sig Signature
This is where most integrations break. To check the signature you must:
- Read the raw request body (don't bind it to a model first).
- Sort the JSON object's keys alphabetically, including keys inside nested objects such as
fee. - Serialize it compactly (no spaces), like JavaScript's
JSON.stringify, keeping numbers exactly as they arrived. - Compute HMAC-SHA512 over that string with your IPN secret and compare it to the header's hex value.
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
public static class NowPaymentsIpnVerifier
{
// NOWPayments signs JSON.stringify(sortedBody) with HMAC-SHA512 using your IPN secret
// and sends the lowercase hex digest in the "x-nowpayments-sig" header.
public static bool IsValid(string rawBody, string? signatureHeader, string ipnSecret)
{
if (string.IsNullOrWhiteSpace(signatureHeader) || string.IsNullOrEmpty(ipnSecret))
{
return false;
}
string canonicalJson;
try
{
canonicalJson = ToSortedJson(rawBody);
}
catch (JsonException)
{
return false;
}
byte[] expected = HMACSHA512.HashData(
Encoding.UTF8.GetBytes(ipnSecret),
Encoding.UTF8.GetBytes(canonicalJson));
byte[] received;
try
{
received = Convert.FromHexString(signatureHeader.Trim());
}
catch (FormatException)
{
return false;
}
// Constant-time comparison so the check doesn't leak timing information.
return CryptographicOperations.FixedTimeEquals(expected, received);
}
// Re-serializes the body with object keys sorted (recursively, ordinal like JavaScript's
// .sort()), no whitespace, and numbers written exactly as they arrived.
public static string ToSortedJson(string rawBody)
{
using JsonDocument doc = JsonDocument.Parse(rawBody);
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping // don't escape '/', '+', '<' etc.
}))
{
WriteSorted(doc.RootElement, writer);
}
return Encoding.UTF8.GetString(buffer.ToArray());
}
private static void WriteSorted(JsonElement element, Utf8JsonWriter writer)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
foreach (JsonProperty property in element.EnumerateObject()
.OrderBy(p => p.Name, StringComparer.Ordinal))
{
writer.WritePropertyName(property.Name);
WriteSorted(property.Value, writer);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (JsonElement item in element.EnumerateArray())
{
WriteSorted(item, writer);
}
writer.WriteEndArray();
break;
default:
element.WriteTo(writer); // strings, numbers (raw text), true/false/null
break;
}
}
}
Why not just deserialize into a Dictionary and serialize again? Round-tripping numbers through double can change how they print (for example 1E-07 instead of 0.0000001), and default escaping turns characters like & or accented letters into \u sequences. Either one changes the string and the HMAC won't match. The version above writes every value exactly as received. This code was tested against a signature produced by NOWPayments' documented JavaScript method (sorted keys + JSON.stringify), including a nested fee object and non-ASCII text.
Step 5: Wire Up the Endpoints
Here is a complete minimal API Program.cs with a checkout endpoint and an IPN endpoint:
// Program.cs
using System.Text.Json;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<NowPaymentsOptions>(builder.Configuration.GetSection("NowPayments"));
builder.Services.AddHttpClient<NowPaymentsClient>((sp, http) =>
{
var options = sp.GetRequiredService<IOptions<NowPaymentsOptions>>().Value;
http.BaseAddress = new Uri("https://api.nowpayments.io/");
http.DefaultRequestHeaders.Add("x-api-key", options.ApiKey);
});
var app = builder.Build();
// 1) Create an invoice and send the buyer to the hosted payment page.
app.MapPost("/checkout/{orderId}", async (string orderId, NowPaymentsClient nowPayments) =>
{
InvoiceResponse invoice = await nowPayments.CreateInvoiceAsync(
priceUsd: 20m, orderId: orderId, description: $"Order {orderId}");
// Store invoice.Id with your order, then redirect.
return Results.Redirect(invoice.InvoiceUrl!);
});
// 2) Receive IPN callbacks. Read the RAW body: the signature is computed over its contents.
app.MapPost("/payments/nowpayments/ipn", async (HttpRequest request, IOptions<NowPaymentsOptions> options, ILogger<Program> log) =>
{
using var reader = new StreamReader(request.Body);
string rawBody = await reader.ReadToEndAsync();
string? signature = request.Headers["x-nowpayments-sig"].FirstOrDefault();
if (!NowPaymentsIpnVerifier.IsValid(rawBody, signature, options.Value.IpnSecret))
{
log.LogWarning("Rejected NOWPayments IPN with an invalid signature. Body: {Body}", rawBody);
return Results.BadRequest();
}
IpnPaymentMessage ipn = JsonSerializer.Deserialize<IpnPaymentMessage>(rawBody)!;
switch (ipn.PaymentStatus)
{
case "finished":
// Funds reached your wallet: fulfill the order (make this idempotent!).
break;
case "partially_paid":
// Buyer sent less than pay_amount: compare actually_paid and decide.
break;
case "failed":
case "expired":
case "refunded":
// Release any reserved stock / mark the order as unpaid.
break;
default:
// waiting, confirming, confirmed, sending: informational only.
break;
}
log.LogInformation("IPN for order {OrderId}: {Status}", ipn.OrderId, ipn.PaymentStatus);
return Results.Ok();
});
app.Run();
Tips for the IPN endpoint:
- Make fulfillment idempotent. NOWPayments can resend the same notification (you can configure retries in the dashboard, and resend one manually). Record the
payment_idand ignore repeats. - Don't trust the success redirect alone. The buyer lands on
success_urlwith anNP_idquery value, but anyone can type that URL. Confirm with a signed IPN or withGET /v1/payment/{payment_id}. - Exclude the endpoint from antiforgery and authentication, and make sure it's reachable over HTTPS from the internet. Callbacks can't reach localhost; use a tunnel or a staging server while developing.
NOWPayments Payment Statuses
| Status | Meaning | What to do |
|---|---|---|
waiting | Invoice created, no funds seen yet | Nothing |
confirming | Payment seen on the blockchain, confirming | Optionally show "payment received" |
confirmed | Enough confirmations | Nothing yet |
sending | NOWPayments is forwarding funds to your wallet | Nothing yet |
partially_paid | Buyer sent less than the quoted amount | Compare actually_paid with pay_amount; contact buyer or accept |
finished | Funds reached your wallet | Fulfill the order |
failed | Payment failed | Mark unpaid |
refunded | Funds were returned to the buyer | Mark refunded |
expired | Nothing was sent in the time window | Release reserved stock |
Monero blocks arrive about every two minutes and outputs need 10 confirmations to be spendable, so expect roughly 20 to 30 minutes between payment and finished.
Testing Your Integration
- Use real but small amounts. Very small payments are rejected; check the minimum first. A few dollars in XMR is enough for an end-to-end test.
- Keep invoices separate from orders. People create invoices they never pay, or pay late, short or twice. Only create (or mark) the order as paid when a verified
finishedIPN arrives. - Log raw IPN bodies (with a logger such as Serilog) while testing, so you can see exactly what NOWPayments sends and debug signature mismatches.
Security Best Practices
- Store the API key and IPN secret outside your repository, and rotate them if they ever leak.
- Reject any IPN whose signature doesn't verify, and never fulfill based on query strings or client-side data.
- Check that the IPN's
price_amountandprice_currencymatch the order you have on file. - Keep your wallet seed phrase offline. NOWPayments never needs it; only your public address.
A Real Use Case
I created Monerica, a directory for a circular Monero economy. After receiving lots of traffic on the site due to a YouTube video on it, I decided to allow sponsored directory listings. A user can go to the directory without an account, select a listing to sponsor, and pay for it in Monero.
In this way, I have created a privacy-focused advertising platform. A user's sponsored listing can go live once the payment is confirmed, and if they leave the page after paying, the IPN notification still arrives so the sponsorship can be activated.
Other Workflows
- Payment instead of invoice:
POST /v1/paymentreturns the deposit address and XMR amount directly, so you can keep buyers on your own page and render the QR code yourself. - Other coins: leave
pay_currencyempty on an invoice and the buyer can choose any coin you've enabled. Monero is the option that keeps both sides private.
Frequently Asked Questions
What is the x-nowpayments-sig header?
It is the HMAC-SHA512 signature of the IPN request body, hex encoded. NOWPayments computes it over the body with its keys sorted alphabetically, using your IPN secret as the key. Recompute it on your server and compare; if it doesn't match, reject the request.
Why doesn't my NOWPayments IPN signature match?
The usual causes are: using the API key instead of the IPN secret, hashing a re-serialized model instead of the raw body, not sorting nested keys, extra whitespace, numbers changed by a double round trip, or non-ASCII characters being escaped differently. The verifier above handles all of these.
Does NOWPayments support Monero (XMR)?
Yes. Monero is a supported coin for both invoices and direct payments. Set pay_currency to xmr and add an XMR payout address in your account.
Which NOWPayments status means the order is paid?
finished. It means the funds have reached your wallet. Handle partially_paid separately, because the buyer sent less than the quoted amount.
Where do I send the customer after creating an invoice?
To the invoice_url field in the response from POST /v1/invoice. It is a hosted page with the address, amount and QR code.
Where To Sign Up
You can create a free account with NOWPayments here.

