Accept a payment in four steps
Create an intent from your server, send the customer to the hosted
checkout (or embed it), and confirm the result with a signed webhook. All amounts are
integer micro-USD — 1000000 = $1.00.
1 · Create a payment intent
From your backend, authenticate with your secret key
(n4npay_sk_…) — created on your app's page in the dashboard, and scoped to that
app. POST the amount; success_url and cancel_url are optional — omit
them and we fall back to the app's default callback URLs (also set on the app's page), or pass
explicit ones to override per intent. The response is the created intent, tagged with your
app_id, plus a checkout_url and a one-time client_secret.
curl https://pay.n4n.io/v1/payment_intents \
-H "Authorization: Bearer n4npay_sk_3f9a2b7c8d1e4f5a6b0c9d8e7f2a1b3c9e4f5a6b0c8d7e2f1a3b9c8d6e4f5a2b" \
-H "Content-Type: application/json" \
-d '{
"amount_microusd": 4990000,
"description": "Pro plan",
"success_url": "https://shop.example/thanks",
"cancel_url": "https://shop.example/cart"
}'
# → 201 Created
# {
# "id": "9b2f6d4e-1c3a-4e8f-9a21-7c5d2e8b4f10",
# "object": "payment_intent",
# "app_id": "3caa9e6b-9b0e-4b7d-9c9b-2b4b6e7d8f10",
# "amount_microusd": 4990000,
# "currency": "USD",
# "description": "Pro plan",
# "metadata": {},
# "status": "requires_payment",
# "success_url": "https://shop.example/thanks",
# "cancel_url": "https://shop.example/cart",
# "expires_at": "2026-07-10T12:34:56Z",
# "created_at": "2026-07-09T12:34:56Z",
# "client_secret": "cs_…",
# "checkout_url": "https://pay.n4n.io/checkout/9b2f6d4e-1c3a-4e8f-9a21-7c5d2e8b4f10?cs=cs_…"
# }
2 · Send the customer to checkout
The simplest integration: redirect the browser to the
checkout_url from the response. We host the payment UI, offering whichever
methods are enabled for your app — PayPal and/or self-custody crypto, configured on your
app's page; after payment we redirect to your success_url.
// after creating the intent server-side: res.redirect(intent.checkout_url);
3 · Or embed the checkout inline
Drop embed.js onto your page and mount the
checkout in a container. The widget appends an iframe pointing at the checkout_url
and calls onSuccess when the payment settles. Your page's origin must be listed in
your app's allowed origins.
<script src="https://pay.n4n.io/embed.js"></script>
<div id="pay-here"></div>
<script>
N4nPay.embed({
checkoutUrl: intent.checkout_url, // from your server
container: "#pay-here",
onSuccess: function (p) { window.location = "/thanks?pi=" + p.intentId; }
});
</script>
embed({checkoutUrl, container, onSuccess})
returns {destroy()} to remove the iframe and its listener. Completion is delivered
over postMessage as {type:"n4npay", event:"succeeded", intent_id} from
the pay.n4n.io origin — the widget validates the origin before invoking your callback.
4 · Verify webhooks
Payments are confirmed asynchronously. Each delivery
carries an N4nPay-Signature header — verify it with your endpoint's signing secret
(whsec_…) before trusting the body. The signed payload is
<t> + "." + rawBody; compare in constant time and reject stale timestamps.
N4nPay-Signature: t=1700000000,v1=hex(hmac_sha256(secret, t + "." + body))
func verify(secret, header, rawBody, now) bool {
// header: "t=<unix>,v1=<hex>"
t, v1 := parse(header) // split on "," then "="
if abs(now - t) > tolerance { return false } // reject replays (e.g. 5m)
expected := hex(hmac_sha256(secret, t + "." + rawBody))
return constant_time_equal(v1, expected)
}
Each app configures its own webhook endpoints (and its own
signing secret) on the app's page in the dashboard — a shared handler across apps can tell
intents apart via data.payment_intent.app_id.
{
"type": "payment_intent.succeeded",
"created_at": "2026-07-09T12:35:04Z",
"data": {
"payment_intent": {
"id": "9b2f6d4e-1c3a-4e8f-9a21-7c5d2e8b4f10",
"object": "payment_intent",
"app_id": "3caa9e6b-9b0e-4b7d-9c9b-2b4b6e7d8f10",
"amount_microusd": 4990000,
"currency": "USD",
"description": "Pro plan",
"metadata": {},
"status": "succeeded",
"success_url": "https://shop.example/thanks",
"cancel_url": "https://shop.example/cart",
"expires_at": "2026-07-10T12:34:56Z",
"created_at": "2026-07-09T12:34:56Z"
}
}
}
Event catalogue
Every event body carries type,
created_at (RFC3339), and a data.payment_intent object — the same
shape returned by the API, app_id included.