Mintcash
Guides

Accept a one-time payment

The complete server-side flow for MintCash integration. No PCI scope on your side.

This is the simplest way to accept a card payment. Your code never touches the card — the customer enters it on the provider's hosted page. PCI scope stays out of your stack.

The flow

Rendering diagram…

Implementation

Create the payment

Send POST /payments from your server. Include the customer's identity (so the same customer is reused across orders) and a returnUrl so the provider knows where to send the browser after.

const r = await fetch("https://sandbox.mintcash.me/payments", {
  method: "POST",
  headers: {
    Authorization: `Basic ${btoa(`${publicKey}:${secretKey}`)}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    externalId: orderId, // your unique order ID — used for idempotency
    amount: 49.99, // major units — dollars, not cents
    currency: "USD",
    returnUrl: `${baseUrl}/order/${orderId}/complete`,
    customer: {
      externalId: customerId,
      email: customer.email,
      name: customer.name,
    },
  }),
});

const { payment, redirectUrl } = await r.json();

The response includes the payment record (status created) and a redirectUrl — the URL of the hosted page on the provider.

Redirect the customer

Send the browser to redirectUrl. From your server-rendered checkout page, this is a 302. From a SPA, it's a window.location.assign(redirectUrl).

return Response.redirect(redirectUrl, 302);

Handle the completion

After the customer finishes, they either land back on your returnUrl or see a completion screen on the payment page, depending on how your account's payment page is configured — confirm the exact behavior with your account manager. Either way, do not fulfil yet. The customer's browser being done tells you nothing about the money; the webhook tells you the money moved.

On your completion page, show a "thanks, we're processing your order" state. Look up the payment by externalId and poll or wait for the webhook:

// GET /order/[orderId]/complete
const payment = await fetch(
  `https://sandbox.mintcash.me/payments/${stored.paymentId}`,
  { headers: { Authorization: ... } }
).then((r) => r.json());

if (payment.status === "succeeded") {
  return renderThankYou();
} else if (payment.status === "failed") {
  return renderFailure(payment.failureMessage);
} else {
  return renderProcessing();   // pending — webhook is on its way
}

Fulfil on the webhook

When payment.succeeded arrives, that's your trigger to ship the order, grant the credits, send the email.

// POST /webhooks/mintcash
import { verifyWebhook } from "@/lib/mintcash";

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-signature");
  if (!verifyWebhook(body, signature, process.env.MINTCASH_SIGNING_SECRET)) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);
  if (event.environment !== process.env.MINTCASH_ENV) {
    return new Response("wrong env", { status: 400 });
  }

  // Dedupe by eventId
  if (await alreadyProcessed(event.eventId)) {
    return new Response("ok", { status: 200 });
  }

  if (event.event === "payment.succeeded") {
    await fulfillOrder(event.data.externalId);
  }

  await markProcessed(event.eventId);
  return new Response("ok", { status: 200 });
}

See signature verification for the full reference implementation.

Embed the payment form

You don't have to send customers away to the full hosted page — the same page can render inside an iframe on your own site, signalling completion back to your page via postMessage. See Embed the checkout for the iframe setup, origin registration, and the full message reference.

Customize the payment page

The page you get is MintCash's default look. For theming the colors and fonts, or if you want to request a completely custom checkout built from your design, see Customize the payment page.

What to test

Exercise these scenarios in sandbox using the cards from the test cards reference:

  • Happy path with the success card — should hit payment.succeeded
  • Failure with a failing card — should hit payment.failed
  • 3DS challenge with the 3DS-enrolled success card — customer sees the challenge, then bounces back
  • Duplicate externalId — second request returns the same payment record
  • Tampered webhook — your endpoint returns 401 and doesn't fulfil

Subscriptions use the same shape

When you need recurring billing, see Set up a subscription — the API and webhook shape are the same; you also pick a billing interval and we handle renewals.