Accept your first deposit
From a secret key to a settled payment, in four steps.
Get a secret key
Every call below authenticates with a secret key,
cnpy_sk_live_.... It stays on your server and is never sent to a browser. See credentials for the other two credential types and where each is allowed to go.Create a payment intent
Call
POST /api/v1/intentsfrom your server, once per end user about to move funds:curl -X POST https://www.canopypay.io/api/v1/intents \ -H "Authorization: Bearer cnpy_sk_live_a1b2c3_<64 hex chars>" \ -H "Canopy-Version: 2026-09-01" \ -H "Content-Type: application/json" \ -d '{ "merchantReference": "order_10024", "metadata": { "userId": "u_7781" } }'201 Createdjson { "intentId": "6f1b9c2e4d8a4f1b8c2e4d8a4f1b8c2e", "inboxAddress": "0x9f2a1c7b3e5d8a604c1f2b9e7d3a5c8b6e4f1d20", "created": true, "priceUnits": null, "oneTime": false, "widgetToken": "2f7a1d9c4b6e8035a1c7d3f6b8e04a2c5d7f9b1e3a6c8d0f2b4e6a8c0d2f4b6e" }This example carries no destination field, so the deposit settles to your account's own payout wallet.
To send the net to your user's wallet instead, see payout destinations. That path needs two things on: your account's per-intent-destinations setting, and a route Canopy has already opted into Dynamic mode.
Hand your user a deposit address
The create response's
inboxAddressis a deposit address your user can fund directly, by manual transfer from any supported chain.For a checkout page instead, mount the widget with the create response's
intentId.That id is the whole browser integration: price, destination, fee and supported chains all resolve server-side from it, so there is nothing else to fetch or forward.
If you bundle, install the package first:
npm install @canopypay/checkout-sdk@0.7.2Then mount it -- or drop in a script tag if you'd rather skip bundling entirely:
checkout.html<script src="https://canopypay.io/v1/canopy.js"></script> <script type="module"> const { intentId } = await ( await fetch("/my-server/create-canopy-intent") ).json(); const handle = Canopy.mount({ target: "#checkout", intentId, merchant: "cnpy_pk_live_abc123", onEvent: (e) => { if (e.type === "paid") showBanner("Payment sent"); }, }); payButton.addEventListener("click", () => { handle.openNow(window.open("", "canopy_checkout", "width=460,height=640")); }); </script>The npm build needs
canopyOrigin; the script tag works it out from its ownsrc. See the SDK reference for why.Open the popup inside the click handler, synchronously. One
awaitbefore the call is enough for the browser to block it -- see popup timing.Prefer checkout rendered inside your own page rather than a popup? That is
surface: "auto", plus one origin to register first -- see embed checkout inline.Watch for settlement
Settlement is confirmed by webhook, not by the browser. See webhooks for the event catalog and signature verification.
Never wire fulfilment to the browser's paid eventThe checkout SDK's client-side
paidevent is a UX signal only. Release goods on a settlement webhook or a server-side status read, never on this event.
What's next
- Payment intents covers the full create-body contract and the one-active-intent-per-wallet rule.
- Need to name a destination per intent? See payout destinations and the two gates around it.
- Every operation, with a worked request and response, lives in the API reference.
- Credentials lists the three types and where each one is allowed to go.