PersonalBusinessEnterprise
    In a rush? Schedule an Appointment
    Fetching local time...
    Anytime CapitalforBusiness
    Pricing

    Get started

    • Overview
    • Quickstart
    • Test mode
    • Guides & recipes
    • Switch from Stripe
    • Import from Shopify
    • Import from WooCommerce
    • Import from Square
    • Import a CSV file
    • Stripe API mapping
    • Pricing
    • Support
    • FAQ

    Payments

    • Checkout & website
    • Guide: Next.js checkout
    • In person (QR)
    • Payment methods
    • Stablecoin checkout
    • Guide: take stablecoins
    • Fraud rules & 3D Secure
    • Dispute evidence
    • International customers
    • Guide: safe refunds
    • Bank transfers on invoices

    Billing

    • Subscription discounts
    • Guide: sell subscriptions
    • Usage billing
    • Guide: report usage

    Money movement

    • Payout options
    • Guide: reconcile payouts
    • Accounting sync
    • Revenue analytics

    Developers

    • Authentication
    • Errors & retries
    • Pagination
    • Idempotency
    • Rate limits
    • Webhooks
    • Verify webhooks
    • Node SDK (preview)
    • CLI (preview)
    • Postman collection

    AI

    • AI agents hub
    • Safe AI agents
    • Custom agents
    • Prompt library

    Integrations

    • Website builders
    • Shopify
    • Shopify orders by invoice
    • WooCommerce
    • Zapier
    • Slack, Discord and Teams

    Reference

    • Payments
    • Refunds
    • Disputes
    • Payment links
    • Invoices
    • Customers
    • Products
    • Subscriptions & plans
    • Discount codes
    • Meters & usage events
    • Payouts & balance
    • Webhook endpoints
    • Events
    • Test helpers
    • OpenAPI spec
    • Error codes
    • Changelog
    • Glossary

    For AI tools

    • llms-full.txt
    • MCP server
    • openapi.json
    Docs menu · Guide: Next.js checkout

    Get started

    • Overview
    • Quickstart
    • Test mode
    • Guides & recipes
    • Switch from Stripe
    • Import from Shopify
    • Import from WooCommerce
    • Import from Square
    • Import a CSV file
    • Stripe API mapping
    • Pricing
    • Support
    • FAQ

    Payments

    • Checkout & website
    • Guide: Next.js checkout
    • In person (QR)
    • Payment methods
    • Stablecoin checkout
    • Guide: take stablecoins
    • Fraud rules & 3D Secure
    • Dispute evidence
    • International customers
    • Guide: safe refunds
    • Bank transfers on invoices

    Billing

    • Subscription discounts
    • Guide: sell subscriptions
    • Usage billing
    • Guide: report usage

    Money movement

    • Payout options
    • Guide: reconcile payouts
    • Accounting sync
    • Revenue analytics

    Developers

    • Authentication
    • Errors & retries
    • Pagination
    • Idempotency
    • Rate limits
    • Webhooks
    • Verify webhooks
    • Node SDK (preview)
    • CLI (preview)
    • Postman collection

    AI

    • AI agents hub
    • Safe AI agents
    • Custom agents
    • Prompt library

    Integrations

    • Website builders
    • Shopify
    • Shopify orders by invoice
    • WooCommerce
    • Zapier
    • Slack, Discord and Teams

    Reference

    • Payments
    • Refunds
    • Disputes
    • Payment links
    • Invoices
    • Customers
    • Products
    • Subscriptions & plans
    • Discount codes
    • Meters & usage events
    • Payouts & balance
    • Webhook endpoints
    • Events
    • Test helpers
    • OpenAPI spec
    • Error codes
    • Changelog
    • Glossary

    For AI tools

    • llms-full.txt
    • MCP server
    • openapi.json
    DocsPaymentsGuide: Next.js checkout

    Accept a payment from a Next.js app

    A complete Next.js App Router integration: a single-use payment link per order made on the server, a redirect to it, and a verified webhook that marks it paid.

    View as MarkdownOpen in Claude

    This guide builds the whole flow in a Next.js App Router app: a Buy button posts to your server, your server creates the order and a single-use payment link for it, the customer pays on Anytime Capital's hosted checkout, and a verified webhook marks the order paid. Every file is complete: paste them into a new app and it runs.

    1. Browser → POST /api/checkout (your server) → POST /api/v1/payment-links with the order id in metadata and the order's Idempotency-Key.
    2. Your server redirects to the link's url; the customer pays by card, Apple Pay, Google Pay, or bank or stablecoin where you've turned them on.
    3. We send payment.succeeded to /api/webhooks/anytime; you verify it and mark the order paid. Never fulfil on the redirect alone: bank payments take days, and customers close tabs.

    1. Create the app and your keys

    Shell
    npx create-next-app@latest shop --ts --app --eslint --no-src-dir --import-alias "@/*" --yes
    cd shop

    In Settings → Developers, create an API key for this site. A restricted key with payment_links write is all this guide needs; add payments read if you'll look payments up. See Authentication.

    .env.local
    # .env.local — never commit this file
    ANYTIME_API_KEY=ak_live_…           # Settings → Developers → Create key
    ANYTIME_WEBHOOK_SECRET=whsec_…      # shown when you add the webhook endpoint
    APP_URL=https://shop.example.com    # your site; https:// to bring customers back after paying

    2. Orders

    Each order gets an Idempotency-Key when it's created and keeps it. If creating the link times out and you try again, the same key returns the first link instead of making a second one.

    lib/orders.ts
    // lib/orders.ts — a stand-in for your database: swap it for a real table.
    import { randomUUID } from "node:crypto";
    
    export type OrderStatus = "pending" | "paid" | "refunded";
    
    export interface Order {
      id: string;
      title: string;
      amountCents: number;
      status: OrderStatus;
      /** Made once per order and reused on every retry, so a retry never makes a second link. */
      idempotencyKey: string;
      paymentLinkId: string | null;
      paymentId: string | null;
    }
    
    // Kept on globalThis so the dev server's reloads don't empty it. One process only.
    const store = globalThis as typeof globalThis & {
      anytimeOrders?: Map<string, Order>;
      anytimeEvents?: Set<string>;
    };
    const orders = (store.anytimeOrders ??= new Map<string, Order>());
    
    /** Event ids already handled: webhooks can arrive more than once. */
    export const handledEvents = (store.anytimeEvents ??= new Set<string>());
    
    export function createOrder(input: { title: string; amountCents: number }): Order {
      const order: Order = {
        id: `order_${randomUUID().slice(0, 8)}`,
        title: input.title,
        amountCents: input.amountCents,
        status: "pending",
        idempotencyKey: randomUUID(),
        paymentLinkId: null,
        paymentId: null,
      };
      orders.set(order.id, order);
      return order;
    }
    
    export function saveOrder(order: Order): void {
      orders.set(order.id, order);
    }
    
    export function findOrder(id: string): Order | null {
      return orders.get(id) ?? null;
    }

    3. Create the payment link

    lib/anytime.ts
    // lib/anytime.ts — calls the Anytime Capital Payments API. Import it from server code only:
    // the key must never reach the browser.
    const API = "https://anytime-capital.com/api/v1";
    
    export interface PaymentLink {
      id: string;
      url: string;
    }
    
    export async function createPaymentLink(input: {
      title: string;
      amountCents: number;
      orderId: string;
      idempotencyKey: string;
      successUrl?: string;
    }): Promise<PaymentLink> {
      const res = await fetch(`${API}/payment-links`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": input.idempotencyKey,
        },
        body: JSON.stringify({
          title: input.title,
          amountCents: input.amountCents,
          singleUse: true, // closes after its first payment: one link per order
          metadata: { orderId: input.orderId },
          ...(input.successUrl ? { successUrl: input.successUrl } : {}),
        }),
        cache: "no-store",
      });
      const body = await res.json();
      if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
      return { id: body.id, url: body.url };
    }
    app/api/checkout/route.ts
    // app/api/checkout/route.ts — makes the order and its payment link, then sends the customer to pay.
    import { createPaymentLink } from "@/lib/anytime";
    import { createOrder, saveOrder } from "@/lib/orders";
    
    export const runtime = "nodejs";
    
    // Prices live on the server: never take an amount from the browser.
    const PRODUCTS = new Map([["consulting-hour", { title: "Consulting — 1 hour", amountCents: 12500 }]]);
    
    export async function POST(request: Request) {
      const form = await request.formData();
      const product = PRODUCTS.get(String(form.get("productId")));
      if (!product) return new Response("Unknown product", { status: 400 });
    
      const order = createOrder(product);
      const appUrl = process.env.APP_URL ?? "";
      const link = await createPaymentLink({
        title: order.title,
        amountCents: order.amountCents,
        orderId: order.id,
        idempotencyKey: order.idempotencyKey,
        // Success URLs must be https://; on http://localhost the customer stays on our confirmation page.
        successUrl: appUrl.startsWith("https://") ? `${appUrl}/orders/${order.id}` : undefined,
      });
      saveOrder({ ...order, paymentLinkId: link.id });
      return Response.redirect(link.url, 303);
    }
    app/page.tsx
    // app/page.tsx — a Buy button. The form posts to your server, not to us.
    export default function Home() {
      return (
        <main>
          <h1>Consulting</h1>
          <form action="/api/checkout" method="post">
            <input type="hidden" name="productId" value="consulting-hour" />
            <button type="submit">Buy one hour, $125.00</button>
          </form>
        </main>
      );
    }
    app/orders/[id]/page.tsx
    // app/orders/[id]/page.tsx — where customers land after paying (with an https APP_URL).
    import { notFound } from "next/navigation";
    
    import { findOrder } from "@/lib/orders";
    
    export const dynamic = "force-dynamic";
    
    export default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {
      const { id } = await params;
      const order = findOrder(id);
      if (!order) notFound();
      return (
        <main>
          <h1>{order.title}</h1>
          <p>{order.status === "paid" ? "Paid. Thank you!" : "Confirming your payment: refresh in a moment."}</p>
        </main>
      );
    }

    4. Confirm payment by webhook

    app/api/webhooks/anytime/route.ts
    // app/api/webhooks/anytime/route.ts — marks orders paid once the payment really went through.
    import crypto from "node:crypto";
    
    import { findOrder, handledEvents, saveOrder } from "@/lib/orders";
    
    export const runtime = "nodejs";
    
    function verified(body: string, header: string, secret: string): boolean {
      const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
      const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
      const signed =
        typeof parts.v1 === "string" &&
        parts.v1.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
      return signed && Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
    }
    
    export async function POST(request: Request) {
      const body = await request.text(); // the raw body: the signature covers these exact bytes
      const header = request.headers.get("Anytime-Signature") ?? "";
      if (!verified(body, header, process.env.ANYTIME_WEBHOOK_SECRET ?? "")) {
        return new Response("Invalid signature", { status: 400 });
      }
    
      const event = JSON.parse(body);
      if (handledEvents.has(event.id)) return new Response(null, { status: 200 });
    
      const payment = event.data;
      const order = typeof payment?.metadata?.orderId === "string" ? findOrder(payment.metadata.orderId) : null;
      // Only trust a payment made on this order's own link, for this order's amount.
      if (order && payment.paymentLinkId === order.paymentLinkId) {
        if (event.type === "payment.succeeded" && payment.amountCents === order.amountCents) {
          saveOrder({ ...order, status: "paid", paymentId: payment.id });
        }
        if (event.type === "payment.refunded" && payment.amountRefundedCents >= payment.amountCents) {
          saveOrder({ ...order, status: "refunded" });
        }
      }
      handledEvents.add(event.id);
      return new Response(null, { status: 200 });
    }

    Register the endpoint in Settings → Developers → Add endpoint with https://your-site/api/webhooks/anytime and the events payment.succeeded and payment.refunded, or from code with the webhook endpoints API. Copy its signing secret into ANYTIME_WEBHOOK_SECRET. Webhook URLs must be public https:// addresses, so while you develop on your own machine, expose it with a tunnel such as ngrok or Cloudflare Tunnel.

    Or register it with the API
    curl -X POST https://anytime-capital.com/api/v1/webhook-endpoints \
      -H "Authorization: Bearer $ANYTIME_API_KEY" \
      -H "Idempotency-Key: shop-webhook-endpoint" \
      -H "Content-Type: application/json" \
      -d '{"url":"https://shop.example.com/api/webhooks/anytime","events":["payment.succeeded","payment.refunded"]}'

    5. Try it

    1. npm run dev, open the site, and press Buy. You land on the hosted checkout.
    2. With a test key (ak_test_…) and Test mode on, pay with a test card: no real money moves. With a live key, pay a small amount with your own card and refund it from Orders.
    3. With your tunnel's https:// address as APP_URL, you come back to /orders/<id> after paying, and it reads “Paid” once the webhook has arrived.
    4. Refund it from Orders in the dashboard, and the payment.refunded event marks it refunded.

    Going to production

    • Swap lib/orders.ts for your database, with a unique index on the event ids you've handled.
    • Keep ANYTIME_API_KEY and ANYTIME_WEBHOOK_SECRET in your host's secret settings, never in code.
    • Want customers to stay on your page? Open the same link as an overlay with the Pay button.
    • Bank payments sit at pending for days: show “confirming” until payment.succeeded arrives, and handle payment.failed.
    PreviousCheckout & websiteNext In person (QR)

    Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.

    On this page

    • 1. Create the app and your keys
    • 2. Orders
    • 3. Create the payment link
    • 4. Confirm payment by webhook
    • 5. Try it
    • Going to production
    Anytime Capital
    (470) 748-2530
    App Store

    Business

    • Business Checking
    • Business Debit Cards
    • Payments
    • Ecommerce Payments
    • Invoicing
    • Developer Docs

    Solutions

    • CPAs & Accountants
    • Financial Advisors
    • Boat & Auto Dealerships
    • Dental & Medical Practices
    • Creators & Coaches
    • All solutions

    Crypto

    • Crypto Wallet
    • Tether Wallet
    • Buy Digital Assets
    • Explore Markets
    • Security & Licenses
    • Compare Platforms
    • Bitcoin ATM Alternative

    Company

    • Our Story
    • Schedule Appointment
    • Careers
    • Media & Press
    • Investor Relations
    • FAQ

    Branches

    • Atlanta, Georgia
    • Miami, Florida
    • Houston, Texas
    • All branches

    Legal

    • Terms & Conditions
    • Privacy Policy
    • Your Privacy Choices
    • Fraud Notice

    © 2026 Anytime Capital LLC. Anytime Capital is a fintech company, not a bank.

    The Anytime Capital Mastercard® Prepaid Card is issued by Pathward®, N.A., Member FDIC. Mastercard® is a registered trademark of Mastercard International Incorporated. All other trademarks and service marks belong to their respective owners.

    Mastercard