Next.js integration
Add the Donations Embed to a Next.js app — App Router or Pages Router. Includes the webhook route handler example.
Next.js integration
This guide adds the Donations Embed to a Next.js application. It covers both the App Router (Next 13.4+) and the Pages Router (still supported), and includes a complete webhook route handler so your server receives signed delivery events.
The embed itself is a web component and runs in the browser only. The wrapper component (<FlockDonate>) must therefore be marked as a Client Component in App Router projects, or imported with SSR disabled in Pages Router projects.
Step 1: Generate sandbox keys
In the 1st Flock Hub:
- Open Donations Embed → Keys.
- Click Create key pair.
- Set environment to Sandbox and name it (e.g.
My Next.js app - sandbox). - Copy both:
- The publishable key (
pk_test_…) — you will paste this in the browser-side component. - The secret key (
sk_test_…) — you will paste this in the server-side webhook handler.
- The publishable key (
Then, in Donations Embed → Webhooks, configure your endpoint URL (you will set this up in Step 4 below) and copy the signing secret that the Hub returns.
Step 2: Install the wrapper and the Node SDK
npm install @1st-flock/donations-embed @1st-flock/donations-embed-react @1st-flock/donations-embed-node
# or
pnpm add @1st-flock/donations-embed @1st-flock/donations-embed-react @1st-flock/donations-embed-node
The wrapper handles the browser embed; the Node SDK handles the server-side webhook verifier.
Set environment variables in .env.local:
NEXT_PUBLIC_FLOCK_PUBLISHABLE_KEY=pk_test_REPLACE_ME
FLOCK_SECRET_KEY=sk_test_REPLACE_ME
FLOCK_WEBHOOK_SECRET=whsec_REPLACE_ME
The NEXT_PUBLIC_ prefix exposes the publishable key to the browser. Secret keys and the webhook signing secret stay server-side.
Step 3: Add the embed to a page
App Router
Mark the page (or just the component using the embed) as a Client Component:
app/give/page.tsx:
"use client";
import { FlockDonate } from "@1st-flock/donations-embed-react";
export default function GivePage() {
return (
<main>
<h1>Give to Example Church</h1>
<p>Thank you for partnering with us.</p>
<FlockDonate
publishableKey={process.env.NEXT_PUBLIC_FLOCK_PUBLISHABLE_KEY!}
mode="inline"
/>
</main>
);
}
If you want the rest of the page server-rendered (for SEO) and only the embed on the client, factor the embed into its own Client Component:
app/give/donate-form.tsx:
"use client";
import { FlockDonate } from "@1st-flock/donations-embed-react";
export function DonateForm() {
return (
<FlockDonate
publishableKey={process.env.NEXT_PUBLIC_FLOCK_PUBLISHABLE_KEY!}
mode="inline"
/>
);
}
app/give/page.tsx:
import { DonateForm } from "./donate-form";
export default function GivePage() {
return (
<main>
<h1>Give to Example Church</h1>
<p>Thank you for partnering with us.</p>
<DonateForm />
</main>
);
}
Pages Router
Use a dynamic import with ssr: false:
pages/give.tsx:
import dynamic from "next/dynamic";
const FlockDonate = dynamic(
() => import("@1st-flock/donations-embed-react").then((mod) => mod.FlockDonate),
{ ssr: false },
);
export default function GivePage() {
return (
<main>
<h1>Give to Example Church</h1>
<FlockDonate
publishableKey={process.env.NEXT_PUBLIC_FLOCK_PUBLISHABLE_KEY!}
mode="inline"
/>
</main>
);
}
The dynamic import with ssr: false keeps <FlockDonate> out of the server bundle, so Next will not try to render it during SSR.
Step 4: Add the webhook route handler
This handler receives signed delivery events from the platform, verifies the signature, and dispatches per event type. Run a tunnel to your dev machine while testing locally — the platform's webhook delivery cannot reach localhost.
App Router
app/api/webhooks/flock/route.ts:
import {
WebhookFormatError,
WebhookSignatureError,
WebhookTimestampError,
verifyWebhook,
} from "@1st-flock/donations-embed-node";
import { NextResponse } from "next/server";
const SECRET = process.env.FLOCK_WEBHOOK_SECRET!;
export async function POST(request: Request): Promise<Response> {
const signature = request.headers.get("flock-signature");
if (!signature) {
return new NextResponse("missing flock-signature header", { status: 400 });
}
// CRITICAL: read the raw body bytes. Do NOT call request.json() — re-serialising
// the JSON would change the bytes and the signature would not match.
const body = Buffer.from(await request.arrayBuffer());
let event: ReturnType<typeof verifyWebhook>;
try {
event = verifyWebhook(body, signature, SECRET);
} catch (err) {
if (
err instanceof WebhookSignatureError ||
err instanceof WebhookTimestampError
) {
// Always 401 for signature failures — never confirm which check failed.
return new NextResponse("verification failed", { status: 401 });
}
if (err instanceof WebhookFormatError) {
return new NextResponse("malformed delivery", { status: 400 });
}
throw err;
}
// Dedup by event.id — delivery is at-least-once.
// (Implement alreadyProcessed/recordEvent against your database.)
const processed = await alreadyProcessed(event.id);
if (processed) {
return new NextResponse(null, { status: 204 });
}
await recordEvent(event.id);
switch (event.type) {
case "donation.completed":
await persistReceipt(event.data.object);
break;
case "donation.refunded":
await reverseReceipt(event.data.object);
break;
case "recurring.activated":
await recordNewSubscription(event.data.object);
break;
case "key.frozen":
await alertOnCall(event.data.object);
break;
default:
// Unhandled event types are still valid deliveries — return 204
// so the platform does not retry.
break;
}
return new NextResponse(null, { status: 204 });
}
// Force the route to run dynamically (no caching) so each delivery is verified.
export const dynamic = "force-dynamic";
Pages Router
Pages Router gives you the raw body via req only if you opt out of body parsing.
pages/api/webhooks/flock.ts:
import {
WebhookFormatError,
WebhookSignatureError,
WebhookTimestampError,
verifyWebhook,
} from "@1st-flock/donations-embed-node";
import type { NextApiRequest, NextApiResponse } from "next";
const SECRET = process.env.FLOCK_WEBHOOK_SECRET!;
// Disable Next's automatic body parsing — we need the raw bytes.
export const config = {
api: { bodyParser: false },
};
async function readRawBody(req: NextApiRequest): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
return Buffer.concat(chunks);
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
): Promise<void> {
if (req.method !== "POST") {
res.status(405).end();
return;
}
const signature = req.headers["flock-signature"];
if (typeof signature !== "string" || !signature) {
res.status(400).send("missing flock-signature header");
return;
}
const body = await readRawBody(req);
let event: ReturnType<typeof verifyWebhook>;
try {
event = verifyWebhook(body, signature, SECRET);
} catch (err) {
if (
err instanceof WebhookSignatureError ||
err instanceof WebhookTimestampError
) {
res.status(401).send("verification failed");
return;
}
if (err instanceof WebhookFormatError) {
res.status(400).send("malformed delivery");
return;
}
throw err;
}
// Dedup + dispatch (same logic as App Router example above).
// ...
res.status(204).end();
}
Step 5: Test locally
- Run a tunnel from your dev machine. Pick any tool that gives you an HTTPS URL pointing at
localhost:3000:ssh -R 80:localhost:3000 serveo.net # or cloudflared tunnel --url http://localhost:3000 - Configure the public URL in Donations Embed → Webhooks in the Hub. Subscribe to at least
donation.completed. The Hub returns a signing secret on save — copy it intoFLOCK_WEBHOOK_SECRETin.env.local. - Restart your Next.js dev server so the new env var is picked up.
- Open
http://localhost:3000/givein a browser. - Walk through a sandbox donation:
| Field | Value |
|---|---|
| Card number | 4111 1111 1111 1111 |
| Expiry | 10/29 (any future date works) |
| CVV | 123 (any 3 digits work) |
| ZIP | Any valid postal code (no AVS validation in sandbox) |
The same canonical sandbox card is documented under Getting Started → Sandbox testing along with the failure-path tester (4000 0000 0000 0002).
- Watch your Next.js server console — you should see the webhook arrive a second or two after the donation completes.
- Open the Hub's Donations tab to confirm the donation appears with a TEST badge.
Step 6: Switch to live
When ready for production:
- Generate a live key pair in the Hub.
- Update
NEXT_PUBLIC_FLOCK_PUBLISHABLE_KEYto thepk_live_…value. - Update
FLOCK_SECRET_KEYto thesk_live_…value. - Configure your production webhook URL in the Hub. The Hub returns a new signing secret; update
FLOCK_WEBHOOK_SECRETto that value. - Re-deploy.
Customisation
For React props, event handlers, themes, and the imperative open / close pattern, see Integration Examples → React app. The wrapper API is identical across React frameworks.
For server-side donation operations (refunds, listing, recurring management), see Server SDKs → TypeScript / Node.
Common pitfalls
Note:
Module not found: Can't resolve '@1st-flock/donations-embed-react'during build. You forgot to install one of the peer dependencies. Runnpm install @1st-flock/donations-embed @1st-flock/donations-embed-react.
Note:
window is not definedduring build / SSR. Your component using<FlockDonate>is being server-rendered. Add"use client";at the top of the file (App Router) or usedynamic(..., { ssr: false })(Pages Router).
Note: Webhook signatures fail with "signature mismatch" but the secret is correct. You called
request.json()somewhere before verifying. Re-serialising JSON changes the bytes — verify against the raw bytes only. Useawait request.arrayBuffer()in App Router, or read fromreqdirectly withbodyParser: falsein Pages Router.
Note:
Cannot find module 'crypto'in the browser bundle. You imported the Node SDK in a client component. Server-side imports (the Node SDK, the webhook verifier) belong in route handlers / server components only. Client components only need@1st-flock/donations-embed-react.
Note: The webhook handler returns 200 but the Hub shows my deliveries as failed. The platform requires a 2xx response WITH the body emptied or matching its budget — return 204 (No Content) explicitly so there is no ambiguity.
Next steps
- Read the Webhooks reference for the full event catalogue and replay protection.
- See Server SDKs → TypeScript / Node for refunds, recurring management, and other server-side operations.
- Add a CSP — see Compliance & Security.