Skip to content

Quickstart

Accept recurring stablecoin payments on Solana without holding funds.

A mandate is the payer’s signed authorization: it names the payer, the payee, the token, a per-period cap, and a period. Charges execute against it. The payer can revoke it on-chain at any time.

There is no “customer” object and no stored card. The mandate is the relationship.

  1. Install

    Terminal window
    npm install @jorvi/sdk
  2. Create a client

    Your secret key stays on your server. It must never reach a browser.

    import { Jorvi } from '@jorvi/sdk';
    const jorvi = new Jorvi(process.env.JORVI_SECRET_KEY!);
  3. Create a mandate

    Amounts are strings in atomic units, never floats. mint is the SPL mint address of the token, so 2.00 USDC at six decimals is '2000000'.

    const mandate = await jorvi.mandates.create(
    {
    payer: payerWalletAddress,
    payee: yourPayoutWalletAddress,
    mint: usdcMintAddress,
    amountPerPeriod: '2000000', // 2.00 USDC
    periodSeconds: 2592000, // 30 days
    },
    { idempotencyKey: 'order-1234' },
    );
  4. Have the payer authorize it

    In the browser. The payer signs once; the cap and the exact terms are bound on-chain at this point.

    import { JorviCheckout, detectInjectedWallet } from '@jorvi/checkout';
    const wallet = detectInjectedWallet(); // Phantom, Solflare, Backpack, and others
    if (!wallet) throw new Error('No Solana wallet found');
    const checkout = new JorviCheckout(
    { baseUrl, mandateId, clientSecret }, // clientSecret comes from your server
    { wallet },
    );
    await checkout.view(); // what the payer is about to approve
    await checkout.authorize((stage) => console.log(stage));
  5. Read what happened

    await jorvi.mandates.listCharges(mandate.id);
    await jorvi.mandates.listRefunds(mandate.id);
    await jorvi.mandates.retrieve(mandate.id);
  6. Refund, if you need to

    The destination is not a parameter. A refund can only go back to the wallet that paid.

    await jorvi.charges.refund(chargeId, {
    amount: '500000', // optional; omit for a full refund
    reason: 'customer request',
    idempotencyKey: 'refund-1234',
    });
  7. Verify the webhook

    Charges settle on-chain, so your server learns about them through a signed webhook. Verify before trusting the payload.

    import { constructEvent, SIGNATURE_HEADER } from '@jorvi/webhooks';
    const event = constructEvent(
    rawBody, // the raw string, not parsed JSON
    req.headers[SIGNATURE_HEADER], // 'jorvi-signature'
    process.env.JORVI_WEBHOOK_SECRET!,
    );

    It throws JorviSignatureError if the header is missing or malformed, if the MAC does not match, or if the timestamp falls outside the tolerance window, which is 300 seconds by default. That last check is what stops a captured request being replayed later.

The payer can revoke on-chain themselves, and it needs no cooperation from you or from us. A merchant can also revoke through the API:

await jorvi.mandates.revoke(mandate.id);

Once revoked, every subsequent charge fails. It is terminal.

jorvi.paymentLinks.create({ name, payee, mint, amountPerPeriod, periodSeconds, trialSeconds });
jorvi.webhookEndpoints.create({ url });
jorvi.events.list({ limit });
jorvi.metrics.get({ from, to });

In test mode there are two helpers for moving time and funding a wallet:

await jorvi.test.fund({ owner, mint, amount });
await jorvi.test.advance(2592000); // jump a period forward

Keys are prefixed by mode, sk_test_ and sk_live_. Test mode never touches mainnet. A dockerised local validator runs the real on-chain program locally, so you can exercise revocation and cap-exceeded paths before talking to anyone.