API
Tell us when someone did the thing. That’s the whole integration — one call, from the code that already knows it happened.
1. Get a key
Send it as a bearer token from your server. Keys starting sk_test_ only touch private tests and are never invoiced; sk_live_spends real budget. A test key is refused on a live Rewarded Action and vice versa, so a test harness left running can’t bill you.
You’ll get keys with your account. Sign in.
2. Install
npm install @mintfall/node3. Capture
Wherever your backend already knows the event happened — the signup handler, the upgrade webhook, the feature flag that just flipped.
import { Mintfall } from "@mintfall/node";
export const mintfall = new Mintfall({
key: process.env.MINTFALL_KEY!, // sk_test_… while you build
action: "ra_1a2b3c",
});
// then, wherever it already happens. "connect_repo" is the requirement's slug,
// not its category — pass the plain email and we hash it before it leaves you.
await mintfall.capture("connect_repo", { email: user.email });That’s it. We match the call to a participant, tick the requirement, and pay them the moment the set is complete. Retry as much as you like: the same event reported twice is a no-op, enforced by a uniqueness constraint rather than by you remembering an idempotency key.
Everything else
Open one when you need it. None of it is required to get the three steps above working.
Same ergonomics from a component. The hook posts to your app, not to us — one route handler you paste in forwards it with the secret key.
npm install @mintfall/reactimport { MintfallProvider, useMintfall } from "@mintfall/react";
// once, near the root — no key here, just where to post
<MintfallProvider endpoint="/api/mintfall">
<App />
</MintfallProvider>;
// anywhere below it
function UpgradeButton() {
const mintfall = useMintfall();
return (
<button onClick={() => mintfall?.capture("connect_repo")}>
Upgrade
</button>
);
}The route handler it posts to, once:
// app/api/mintfall/route.ts
import { Mintfall } from "@mintfall/node";
import { auth } from "@/lib/your-auth";
const mintfall = new Mintfall({
key: process.env.MINTFALL_KEY!,
action: "ra_1a2b3c",
});
export async function POST(request: Request) {
// The slug the browser named. It is the only thing the browser gets to say.
const { event } = await request.json();
// You already know who this is — that's the point of forwarding through here.
const user = await auth();
if (!user) return new Response(null, { status: 401 });
await mintfall.capture(event, { email: user.email });
return Response.json({ ok: true });
}The indirection is not ceremony. A browser holding a key that mints payouts is money anyone can print — so the client tells your server, and your server, which already knows who is signed in, tells us. That is also why there is no <script>tag version and won’t be.
capture() takes the slug you named when you published — connect_repo — and it has to be one this Rewarded Action actually asks for. Anything else is a 422 naming the ones that would have worked, rather than a silently ignored call. Open a Rewarded Action to see its exact slugs.
The five below are categories, which is a different thing: you pick one per requirement on the create form, and it decides the verb in the published sentence. Passing a category to capture() is the commonest integration mistake — only a bare signup or return ever has a slug equal to its category.
signup— Create an account on your product. e.g. “Create an account”visit— Reach a specific page. e.g. “Earnings”click— Press a button or finish a flow. e.g. “Connect a repository”convert— Start a trial, subscribe, or buy. e.g. “Pro plan”return— Return on a later day — the one that shows real retention. e.g. “Come back the next day”
Through the packages you pass email, the plain address, and they hash it before the request leaves you — so a readable address never reaches us. There is deliberately no emailHash option on capture(): one normalisation in one place is what stops attribution failing silently.
Calling /api/v1/events directly — curl, or another language — you send emailHash yourself: sha256 of the lowercased, trimmed address. @mintfall/node exports hashEmail for exactly that. emailis accepted too and hashed on receipt; we don’t store it and don’t log the body, but hashing your side is strictly better.
The failure mode worth knowing about before you rely on it: email matching misses when someone’s address on your product differs from the one on their Mintfall account, and it misses silently — they do the work and see no checkmark. If that matters, also pass ref. It arrives on the participant’s click-through link as ?mf=…; store it on their row and send it back. When present it wins, because it’s exact.
Create a private test from the home tab and point your sk_test_key at it. It behaves exactly like the real thing — the checkmarks, the payout row — and is never invoiced. Test payouts also never reach a participant’s withdrawable balance.
A test is allow-listed, and your own address is always on the list. You complete it from your user account— the same address, the other account. Sponsor accounts can read every Rewarded Action but can’t complete one, which is the point: the side that pays and the side that gets paid are never the same session.
To rehearse with colleagues, add their addresses when you publish, or later from the Rewarded Action’s own page — up to 20 including you. Each of them needs their own free user account on that address first: a reported event is matched to a participant account, so allow-listing decides who may run it, not who exists. Nobody outside the list can see it or complete it, and removing an address takes both away.
Stuck on something we don’t support yet? Ask — we answer integration questions within 24 hours.