Skip to main content

Guides

Next.js App Router Integration

This guide shows you exactly where to add RecurCite SDK calls in a Next.js 13+ application using the App Router. By the end, you'll have full evidence coverage for disputes.

Prerequisites

  • Next.js 13+ with App Router
  • @recurcite/sdk installed (npm install @recurcite/sdk)
  • A RecurCite API key from Dashboard → Developers
  • Stripe Billing set up with webhook forwarding

1. Create the SDK singleton

Create a single file that initializes the RecurCite client. Import it anywhere you need to send events:

lib/recurcite.ts
import { init } from "@recurcite/sdk";

export const recurcite = init({
  apiKey: process.env.RECURCITE_API_KEY!,
  signingSecret: process.env.RECURCITE_SIGNING_SECRET,
});

Server-side only

This file should only be imported in server components, API routes, and server actions. Never import it in client components — it would expose your API key.

2. Track terms acceptance

When a user accepts your Terms of Service, send a terms.accepted event. This is the single strongest piece of evidence — it proves the customer agreed to your policies.

app/api/accept-terms/route.ts
import { recurcite } from "@/lib/recurcite";
import { auth } from "@/lib/auth";

export async function POST(req: Request) {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { termsVersion } = await req.json();

  await recurcite.track({
    type: "terms.accepted",
    payload: {
      version: termsVersion,
      accepted_at: new Date().toISOString(),
    },
    stripe_refs: {
      stripe_customer_id: session.user.stripeCustomerId,
    },
  });

  return Response.json({ success: true });
}

3. Track logins

Send a user.login event every time a user authenticates. If you use NextAuth/Auth.js, add it to the signIn callback:

lib/auth.ts (signIn callback)
import { recurcite } from "@/lib/recurcite";

// Inside your NextAuth config:
callbacks: {
  async signIn({ user, account }) {
    // Track login for evidence
    if (user.stripeCustomerId) {
      await recurcite.track({
        type: "user.login",
        payload: {
          occurred_at: new Date().toISOString(),
          // IP and user_agent come from the request
          // You may need to pass these through context
        },
        stripe_refs: {
          stripe_customer_id: user.stripeCustomerId,
        },
      });
    }
    return true;
  },
}

Tip

Alternatively, track logins in a dedicated API route so you can access req.ip and req.headers["user-agent"]. IP addresses are especially valuable for CE3 matching.

4. Track product usage

Send product.used events for key feature usage. This proves the customer derived value from your service:

app/api/track-usage/route.ts
import { recurcite } from "@/lib/recurcite";
import { auth } from "@/lib/auth";

export async function POST(req: Request) {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { featureKey, count } = await req.json();

  await recurcite.track({
    type: "product.used",
    payload: {
      feature_key: featureKey,
      count: count || 1,
      occurred_at: new Date().toISOString(),
    },
    stripe_refs: {
      stripe_customer_id: session.user.stripeCustomerId,
    },
  });

  return Response.json({ success: true });
}

For high-throughput usage, batch events to reduce API calls:

lib/usage-tracking.ts
import { recurcite } from "@/lib/recurcite";

export async function trackDailyUsage(
  stripeCustomerId: string,
  features: { key: string; count: number }[]
) {
  await recurcite.batchTrack(
    features.map((f) => ({
      type: "product.used" as const,
      payload: {
        feature_key: f.key,
        count: f.count,
        occurred_at: new Date().toISOString(),
      },
      stripe_refs: { stripe_customer_id: stripeCustomerId },
    }))
  );
}

5. Track cancellations

Track both the cancellation request and confirmation. This proves you had a proper cancellation flow:

app/api/cancel-subscription/route.ts
import { recurcite } from "@/lib/recurcite";
import { auth } from "@/lib/auth";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const session = await auth();
  const { subscriptionId } = await req.json();

  // 1. Track cancellation request
  await recurcite.track({
    type: "cancellation.requested",
    payload: { occurred_at: new Date().toISOString() },
    stripe_refs: {
      stripe_customer_id: session!.user.stripeCustomerId,
      stripe_subscription_id: subscriptionId,
    },
  });

  // 2. Cancel in Stripe
  const sub = await stripe.subscriptions.cancel(subscriptionId);

  // 3. Track cancellation confirmed
  await recurcite.track({
    type: "cancellation.confirmed",
    payload: {
      occurred_at: new Date().toISOString(),
      receipt_id: sub.id,
    },
    stripe_refs: {
      stripe_customer_id: session!.user.stripeCustomerId,
      stripe_subscription_id: subscriptionId,
    },
  });

  return Response.json({ success: true });
}

6. Track transactions for CE3

For CE3 eligibility, send a transaction.completed event on every successful charge. The best place is your Stripe webhook handler for invoice.paid:

app/api/stripe/billing-webhook/route.ts (excerpt)
case "invoice.paid":
case "invoice.payment_succeeded": {
  const invoice = event.data.object as Stripe.Invoice;

  await recurcite.track({
    type: "transaction.completed",
      payload: {
        charge_id: invoice.charge as string,
        amount: invoice.amount_paid,
        currency: invoice.currency,
        occurred_at: new Date(invoice.created * 1000).toISOString(),
        // CE3 signals — include as many as possible:
        ip: customerIp,           // from your session/auth
        email_sha256: customerEmailSha256, // SHA-256(email)
        product_description: invoice.lines.data[0]?.description ?? "",
      },
    stripe_refs: {
      stripe_customer_id: invoice.customer as string,
      stripe_subscription_id: invoice.subscription as string,
      stripe_invoice_id: invoice.id,
    },
  });
  break;
}

CE3 signal importance

CE3 requires at least 2 prior undisputed transactions with matching IP or email signals. Send this event on every charge to build a strong signal history. See Concepts → CE3 for full details.

Environment variables

.env.local
# RecurCite
RECURCITE_API_KEY=rc_live_your_api_key_here
RECURCITE_SIGNING_SECRET=your_signing_secret_here  # Optional but recommended

Integration checklist

EventWherePriority
terms.acceptedToS checkbox / acceptance endpointCritical
user.loginNextAuth signIn callback or login API routeCritical
product.usedFeature usage API routes / middlewareHigh
transaction.completedStripe invoice.paid webhookHigh (CE3)
cancellation.*Cancel subscription endpointHigh
support.ticket.*Support system webhooks or APIMedium

Next steps