Skip to content

Start

Quick start

Your first payment in five minutes — first without code, then with two calls and a webhook.

Everything here works in test mode: test networks, test coins, nothing real moves. Switch to live when it feels right.

  1. 1

    Sign up

    Start free with your email. No password — we send a code.

  2. 2

    Connect a wallet

    Dashboard → Wallets. Add the public key from your Ledger or Trezor. See Wallets.

  3. 3

    Make a product

    Dashboard → Products. A name and a price is enough.

  4. 4

    Make a link

    Dashboard → Links → New. Copy the /l/… address.

  5. 5

    Pay it

    Open the link, pick a coin, and press Simulate payment. The invoice turns Paid and a receipt is sent.

That's a real checkout. Share the same kind of link in live mode and buyers pay straight to your wallet.

With code: two calls

Get a test key in Dashboard → Developers. It starts with sk_test_. Keep it on your server.

Install
npm install perly-pay
import { PerlyPay } from "perly-pay";

const pay = new PerlyPay(process.env.PERLY_PAY_KEY); // sk_test_…

// 1. Make an invoice (amounts in cents)
const invoice = await pay.invoices.create({
  amount: 2400,
  currency: "USD",
  description: "Beans, 1 kg",
  customer: { email: "ada@example.com" },
  metadata: { orderId: "1042" },
});

// 2. Send the buyer to pay it
redirect(invoice.url); // https://pay.perly.io/i/<uuid>

The buyer picks bitcoin or a stablecoin, scans, pays. The money goes to an address made from your key.

Know when it's paid: a webhook

Add an endpoint in Dashboard → Developers → Webhooks and copy its whsec_… secret. Then check every delivery's signature before you trust it.

server.js
app.post("/webhooks/perly", express.raw({ type: "application/json" }), (req, res) => {
  const event = pay.webhooks.verify(
    req.body,                                // the raw bytes, not parsed JSON
    req.header("Perly-Signature"),
    process.env.PERLY_PAY_WEBHOOK_SECRET,
  ); // throws if the signature is wrong or old

  if (event.type === "invoice.fulfilled") {
    ship(event.data.object.metadata.orderId);
  }
  res.sendStatus(200);
});
Ship on invoice.fulfilled, not on invoice.paid: fulfilled comes once, after screening. See Webhooks.

Go live

  • Connect your live wallet keys (mainnet).
  • Make a live key (sk_live_…) and a live webhook endpoint.
  • Test mode stays as it is, beside live. The two never mix.