On this page
- Webhook logic is testable offline: signWebhook and sampleEvent from @fianto/sdk/webhooks sign a sample exactly as Fianto does.
- fianto trigger <type> --forward-to <local url> posts a signed sample of any of the 14 event types to your route, with a fresh event id each run and no API call.
- The payment path needs mainnet: a one-off 0.01 USDC payment from your own second wallet costs a Solana network fee under $0.01, and the 0.01 USDC lands back in your receiving wallet.
Why Fianto has no sandbox
A Fianto deployment runs on exactly one Solana cluster, and the hosted one at api. has run on mainnet since 6 October 2026. A test mode would need a second deployment on devnet with its own merchants, plans and wallets, and Fianto does not run one. The docs say it in their first line on the subject: "Fianto has no test mode and no test keys." Every secret starts with fian_: there is no test key to switch to.
That sounds worse than it is. Most integration bugs live in your webhook route: a missed event type, a double grant because an event arrived twice, a body parsed before verification. None of those need money to find. What does need mainnet is the part you do not write, the hosted checkout and the wallet, and one real payment covers it.
Which tool tests which part
| Tool | What it proves | Needs | Cost |
|---|---|---|---|
A unit test with signWebhook | Your handler verifies, routes and dedupes each event type | Nothing but your test runner | Free |
fianto trigger <type> --forward-to | Your running route accepts real-shaped, signed samples | The webhook secret only | Free, no API call |
fianto trigger test. | Fianto can reach your public URL and your signature check passes | App id and secret, a verified webhook URL | Free, 10 an hour per application |
fianto events tail --forward-to | Your local route handles your real events | App id, secret and webhook secret | Free |
| One real 0.01 USDC payment | Session creation, the checkout, the wallet and order.paid end to end | An approved account, a second wallet with USDC and a little SOL | A network fee under $0.01 |
| One real 0.01 USDC subscription | Plan creation, the subscribe transaction and subscription.created | The same, plus SOL for account rent | 0.01 USDC network fee today, plus non-refundable wallet setup rent |
Work down the table. The first two rows make no API call, so they work before your merchant account is approved. test. and events tail call the API, which answers 401 to every key until approval.
Unit-test the webhook route with signed samples
@fianto/sdk/webhooks exports sampleEvent(type), a realistic event of any type with obviously fake ids such as fian_, and signWebhook({ event, secret }), which signs it the way Fianto signs a delivery and returns the body and headers. Feed both to your route handler as a plain Request. This test assumes the Next.js route from accepting USDC in a Next.js app, with its data layer and the SDK client stubbed:
import { beforeAll, expect, it, vi } from 'vitest';
import { sampleEvent, signWebhook } from '@fianto/sdk/webhooks';
// The route re-reads each subscription through the SDK and saves it: stub both.
vi.mock('@/lib/billing', () => ({ saveSubscription: vi.fn(async () => undefined) }));
vi.mock('@fianto/sdk', async () => {
const { sampleEvent } = await import('@fianto/sdk/webhooks');
return {
Fianto: class {
subscriptions = { retrieve: async () => sampleEvent('subscription.created').data };
},
};
});
// Any whsec_ key of 16 bytes or more; this one never leaves the test.
const SECRET = `whsec_${Buffer.alloc(32, 7).toString('base64')}`;
let POST: (request: Request) => Promise<Response>;
beforeAll(async () => {
process.env.FIANTO_WEBHOOK_SECRET = SECRET; // set before the route module loads
({ POST } = await import('../app/api/webhooks/fianto/route'));
});
const deliver = (body: string, headers: Record<string, string>) =>
POST(new Request('http://localhost/api/webhooks/fianto', { method: 'POST', headers, body }));
it('accepts a signed subscription.created', async () => {
const event = sampleEvent('subscription.created', { id: `evt_${'a'.repeat(32)}` });
const { body, headers } = await signWebhook({ event, secret: SECRET });
expect((await deliver(body, headers)).status).toBe(200);
});
it('refuses a body changed after signing', async () => {
const event = sampleEvent('subscription.ended', { id: `evt_${'b'.repeat(32)}` });
const { body, headers } = await signWebhook({ event, secret: SECRET });
expect((await deliver(body.replace('sample', 'forged'), headers)).status).toBe(400);
});Add one test per event type you handle, and one that delivers the same event twice and checks you granted access once. Delivery is at least once, so the second case is not hypothetical. Why the tampered body fails, and what the signature covers, is in verifying Standard Webhooks signatures. A grant keyed by subscription and period, as in accepting USDC for an AI app, passes the duplicate test by design.
Replay every event type with the CLI
The @fianto/cli package installs a fianto command. With --forward-to, trigger signs a local sample and posts it to your running dev server; it makes no API call and needs only the webhook secret. Without --forward-to it sends nothing at all, except for test., and any other type exits with code 2.
# A signed sample to your local route. Each run gets a fresh event id. npx @fianto/cli trigger subscription.past_due \ --forward-to http://localhost:3000/api/webhooks/fianto \ --secret "$FIANTO_WEBHOOK_SECRET" # A real, signed test.event from Fianto to your verified public URL. npx @fianto/cli trigger test.event # Your application's real events, re-signed and posted to your machine. npx @fianto/cli events tail \ --forward-to http://localhost:3000/api/webhooks/fianto \ --since 1h
trigger --forward-toaccepts onlylocalhost,127.or0. 0. 0/8 [::1]unless you add--allow-remote, because a signed sample at a public URL looks like a real delivery. It prints→ 200 subscription.and exits 1 on any status that is not 2xx.past_ due (local sample) trigger test.needsevent FIANTO_andAPP_ ID FIANTO_exported in the shell or passed as flags (the CLI does not readAPP_ SECRET .files) and a verified webhook URL. Its data isenv { "message": "This is a test event from Fianto.. Ten an hour per application, shared with the dashboard's test button." } events tailpolls every 2 seconds by default and forwards each event once per run, with no retry. It is a development aid; real deliveries still go to your verified URL.
Fianto never sends a business event such as order. or subscription. on request. Those come only from real payments, which is the next step.
Run one real payment for 0.01 USDC
A real payment on mainnet is the only way to see the hosted checkout, a real wallet signature and a real order.. Keep it small: 0.01 USDC is the lowest price Fianto accepts. Pay from a second wallet you control, so the money comes back to you.
- Get the merchant account approved. Until then the API refuses your keys.
- Fund a second wallet with a little USDC and some SOL. The checkout tells you the exact SOL the wallet needs; for a one-off payment that is the wallet's own rent-exempt minimum (650,240 lamports when measured on 15 October 2026), the network fee and a small margin.
- Create a
mode: paymentsession withamount: '0.and a description, through your own checkout route or the SDK.01' - Open it, connect the second wallet and sign. The page shows success at confirmed;
order.andpaid checkout.arrive at finalized.session. completed - Check that your route stored the order once, then find the payment under Payments in the dashboard and the transfer in your receiving wallet.
The whole run costs the second wallet a Solana network fee under $0.01. The 0.01 USDC price lands in your receiving wallet, and there is no Fianto fee during the beta; after it, the customer pays about 0.1 to 0.5% on top, and the merchant still pays nothing.
Testing a subscription for real
A subscription test costs a little more and teaches less, because the first renewal comes 30 days and two minutes after the start and nothing on mainnet fast-forwards it. Create a monthly price of 0.01 USDC in the dashboard, then a subscription session for it. The first session creates the price's plan on Solana, so the page shows the plan as preparing for a moment. The subscribe transaction then charges the first period: 0.01 USDC to you plus a network fee of 0.01 USDC today to Fianto's treasury, on plans created since 6 October 2026.
The wallet also pays account rent in SOL, read live at subscribe time and shown on the checkout. As of 15 October 2026 the subscription account took about 0.00144 SOL, returned when the wallet reclaims it after the subscription ends, and the one-time wallet setup account about 0.00119 SOL, not returned. To end the test, cancel from your server with fianto. and watch subscription. arrive with end_. That cancel changes nothing on Solana, so to get the rent back the wallet uses "Cancel on Solana" in the portal at fianto.xyz/my, then reclaims it once the paid period has ended on chain, up to 30 days later for a monthly test.
Signed webhooks, the CLI and the SDKs on one page.
Open the developer overviewCan you test with devnet USDC instead?
No: devnet USDC cannot pay a Fianto checkout, because the hosted API at api. settles on mainnet and a devnet token is a different token from its mainnet namesake. Circle's address list gives the devnet USDC mint as 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU and the mainnet mint as EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (checked on 15 October 2026). A mint is the on-chain account that defines a token, and a checkout accepts only the one its cluster uses.
So the test plan stays the one above: signed samples for every event type, one test. for the network path, and one small real payment for the checkout. Every CLI flag and exit code is on the docs page for the Fianto CLI.
A pre-launch checklist
- Each event type you handle has a unit test, plus a duplicate-delivery test.
- A
trigger --forward-torun returns 200 for every type, and a run with the wrong secret returns 400. - A real
test.reached your production URL and shows inevent fianto events list --type test..event - Your handler answers within 10 seconds; anything slower counts as a failed attempt and is retried, up to 8 attempts over about 28 hours.
- One real 0.01 USDC payment produced exactly one fulfilment.
- Your success page says "confirming", not "paid": the browser's result is not proof of payment.
If the one real payment shows money in your wallet but no paid order, the order can still turn paid at finalized; how a merchant knows a crypto payment arrived walks through those cases.